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:
# 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
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| Action | When a matching file changes |
|---|---|
sync | copy it into the running container at target |
rebuild | rebuild the image and recreate the container |
sync+restart | copy it in, then restart the container (for apps that only read config at startup) |
restart | restart the container without copying |
See it work
$ docker compose watch...Container notes-api-1 HealthyWatch 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:
Syncing service "api" after 2 changes were detected
and the API's logs show Node restarting itself:
$ docker compose logs api --since 30sapi-1 | Restarting 'src/server.ts'api-1 | SIGTERM received, closingapi-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:
Rebuilding service(s) ["api"] after changes were detected...Image notes-api BuildingContainer notes-api-1 RecreateContainer 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. Onlysrcis 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_modulesyou have to fix by hand.
You add a new npm package with npm install on your machine while docker compose watch is running. What happens?
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
How would you set up a fast local development loop for a containerised service?