Using docker compose watch with Node.js
Tags: node, docker, watch, compose
Published: 2024-05-12 11:24:46

I was recently working on a small side project when I hit a roadblock. I wanted to use a package but was getting some error while installing in my Windows system. So, I decided to use docker as a development environment. Now docker can be really great for this task and if you use volume and bind mounts you can also set up a _reload on save_ pretty easily.

When I was about to do the setup, I remembered that with Docker Compose version 2.22.0, `docker compose watch` was released and as per [this docs page](https://docs.docker.com/compose/file-watch/):

> Use watch to automatically update and preview your running Compose services as you edit and save your code.

This seemed like the perfect opportunity to try this!
Furthermore, with the release of Node v22, `--watch` mode was no more an experimental feature, so another cool thing to include 😊

You can check the whole code in this [github repo](https://github.com/azhar1038/compose-watch-js).

## Setup

### Install Docker
Since I am using windows, I use Docker Desktop which you can install following this guide: [Install Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/)
**NOTE:** To install Docker Desktop in Windows, you must install WSL2 first for which you may follow this guide: [How to install Linux on Windows with WSL](https://learn.microsoft.com/en-us/windows/wsl/install)

If you use Mac, follow this: [Install Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/)

If you use Linux, you can either install Docker Desktop or you can manually install just Docker Engine and Docker Compose. If you want to install Docker Desktop, check this guide: [Install Docker Desktop on Linux](https://docs.docker.com/desktop/install/linux-install/)

**NOTE:** Docker Desktop in simple terms is basically a VM + Docker Engine + GUI + extra features like Compose, Kubernetes, Credential helper etc.

### Write a simple Express server

1. Create a new folder `compose-watch`
2. Go to `compose-watch` folder and terminal there
3. Run `npm init -y`. This will instantiate a new node project
4. Install `express` using `npm i express`
5. Create a new folder `src` in the root of your project
6. Create a new file `src/index.js` and add the following content:
```js
import express from "express";

const app = express();

app.get("/", (req, res) => {
  res.json({ msg: "Hello World" });
});

app.listen(3000, () => {
  console.log("Server is listening at http://localhost:3000");
});
```

Now add scripts in package.json and change type to `module` as we are using ES6 imports:
```json
{
  "name": "compose-watch",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "^4.19.2"
  }
}
```

Run `npm run dev` to start the server in watch mode.
Now open any browser and type the url `http://localhost:3000` and you will get the Hello World response.
Now try changing the Hello World message in `src/index.js` and you will notice server restarts and now new value appears after calling the url again!

With this we have a working server. So, it's time to dockerize!

### Write the Dockerfile

Create a new file in project root called `Dockerfile` and add the following content:

```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json .
RUN npm install
COPY ./src /app/src
CMD npm run dev
```

### Write the compose file

Create a new file in project root called `compose.yml` and add the following content:

```yml
services:
  server:
    build:
      context: .
    ports:
      - 3000:3000

    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

        - action: rebuild
          path: package.json
          target: /app
```

### Time to run!
Now we are ready to test. Run the following command to build the image and start the container in watch mode:
```
docker compose watch
```
And that's it!
This will start the server in watch mode. If you make a change in message again, you will be able to see the updated message.

But wait! It seems like watch works only once?
When I try to change the message again, it is not reflecting in the  server even though the terminal says
```
Syncing "server" after changes were detected
```

I am not sure why exactly this issue happens, but it seems like node native `--watch` flag does not work properly 😞

### Switching to nodemon
1. Install nodemon using `npm i --save-dev nodemon`
2. Change the dev script in `package.json` to `nodemon src/index.js`

So, now `package.json` looks like this:
```json
{
  "name": "compose-watch",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}
```
Now restart the server using `docker compose watch` command.
It seems like compose watch works perfectly with nodemon.

If you have used nodemon before with docker volumes, you might know that we need to use polling for nodemon to work properly with docker using the `-L` flag but we don't need that with compose watch.

So, this is how you can use docker as a development environment (at least for now 😅). If any of you know how to fix the issue with `--watch` flag, do let me know!

Again, whole code is available in [github repo](https://github.com/azhar1038/compose-watch-js).

Thanks for reading!