sehaj/learn
Docker/06 Compose

06.05 · with code · 12 min

A dev loop with compose watch

Edit code on your machine and see it running in the container a second later, without bind-mount pitfalls or rebuilding by hand.

The core idea

During development you want edits to show up immediately. Compose watch watches folders on your machine and, when something changes, either copies the changed files into the running container or rebuilds the image and replaces the container, depending on rules you write. Combined with a process that restarts on change inside the container, you get hot reload.

A dev stage

The production image compiles TypeScript and ships only dist. For development, the notes API's Dockerfile now has a dev stage that keeps the source and dev dependencies and runs a watcher:

api/Dockerfile
# Development (06.05): the build stage plus a watcher. Node 22 runs TypeScript directly by
# stripping the types, so edits restart the server with no compile step. Build with --target dev.
FROM build AS dev
ENV NODE_ENV=development
CMD ["node", "--watch", "src/server.ts"]

Two Node features do the work: node --watch restarts the process when a file it loaded changes, and Node 22 (from 22.18) runs .ts files directly by stripping type annotations. The final production stage is still last in the file, so a plain docker build produces the same image as before. Compose picks the dev stage with target: dev.

Watch rules

compose.yaml
api:
  build:
    context: ./api
    target: dev
  develop:
    watch:
      # Source edits are copied into the running container; node --watch restarts the server.
      - action: sync
        path: ./api/src
        target: /app/src
      # New dependencies need a new image.
      - action: rebuild
        path: ./api/package.json
ActionWhen a matching file changes
synccopy it into the running container at target
rebuildrebuild the image and recreate the container
sync+restartcopy it in, then restart the container (for apps that only read config at startup)
restartrestart the container without copying

See it work

❯terminal
$ docker compose watch
...
Container notes-api-1 Healthy
Watch enabled
$ curl localhost:8080/healthz # in another terminal
{"status":"ok"}

Now edit api/src/server.ts so /healthz returns { status: "ok", edited: true } and save. Watch prints:

❯terminal
Syncing service "api" after 2 changes were detected

and the API's logs show Node restarting itself:

❯terminal
$ docker compose logs api --since 30s
api-1 | Restarting 'src/server.ts'
api-1 | SIGTERM received, closing
api-1 | notes-api listening on :3000 (max note length 280)
$ curl localhost:8080/healthz
{"status":"ok","edited":true}

A change to package.json, like adding a dependency, triggers the other rule:

❯terminal
Rebuilding service(s) ["api"] after changes were detected...
Image notes-api Building
Container notes-api-1 Recreate
Container notes-api-1 Recreated

docker compose up --watch does the same as up plus watch, with every service's logs in one terminal.

Why not a bind mount?

You could bind-mount ./api over /app (04.03 Bind mounts). Watch avoids the traps that come with it:

  • No hidden node_modules. Only src is synced; the image's dependencies stay put.
  • No host-built native modules leaking into a Linux container.
  • Faster on Mac and Windows, because only changed files cross into the VM instead of every read going through file sharing.
  • Dependency changes rebuild properly, instead of a stale node_modules you have to fix by hand.
Predict first

You add a new npm package with npm install on your machine while docker compose watch is running. What happens?

Commit to an answer before you read on.
CheckpointHot reload the notes API

Run docker compose up --watch. Change the startup log message in server.ts and watch it restart. Add a new route, GET /version returning the version from package.json, and call it through the proxy. Finally bump the version in package.json and watch the rebuild.

STUCK? ▸

Edits sync but nothing restarts: the container is running the production stage. Check target: dev and run docker compose up --build once. Nothing syncs at all: watch only sees paths inside the path: you gave, relative to the compose file.

Recap

Interview questions

MidfreqGood to knowDesign

How would you set up a fast local development loop for a containerised service?

Report a mistakeEdit this page
☰ Compose06.05 · 36/44next →