Docker Compose Watch: Instant Feedback for Local Container Dev

Photo by Florian Olivo

Photo by Florian Olivo
For years my local container workflow had a tax on every change: edit a file, switch to the terminal, run docker compose up --build, wait thirty seconds to two minutes, then check the browser. Multiply that by the eighty or so changes I make in a focused afternoon and I was burning serious time just waiting on rebuilds. Docker Compose Watch removes that tax almost entirely, and it ships in the Compose binary you already have.
The short answer for anyone searching: add a develop.watch block to your compose.yaml, declare which paths should sync, rebuild, or sync+restart, and run docker compose up --watch. Compose Watch went GA in Compose v2.22.0 (bundled with Docker Desktop 4.24), so if you have updated Docker any time since late 2023, you already have it. In this post I will walk through how I use it across a NestJS API and worker setup, where it beats bind mounts, and the gotchas that bit me in real projects.
Before Watch, you had two options for live code in containers, and both had sharp edges:
Compose Watch takes a third path: it observes your host files and performs a one-way copy of changes into the running container. Your image stays the single source of truth for dependencies and system libraries, while your source code flows in at save-time. Container-side changes never leak back to your host, which is exactly the direction of trust you want in development.
Every watch rule pairs a path with an action. Choosing the right action per path is where the real speedup lives, because the cheapest action that still gives you a correct container wins.
| Action | What it does | When I use it |
|---|---|---|
| sync | Copies changed files into the running container at the target path, without restarting anything. | Source code in anything with hot reload: NestJS with webpack HMR, Next.js, Vite, nodemon-watched scripts. |
| rebuild | Rebuilds the image with BuildKit and replaces the running container, like docker compose up --build for that service. | package.json, lockfiles, Dockerfile, anything baked into image layers. Also compiled languages like Go where the binary is the artifact. |
| sync+restart | Copies the changed files, then restarts the container process without rebuilding the image. | Config files read once at boot: .env files, nginx.conf, ormconfig. The image is fine, the process just needs a fresh start. |
Here is a trimmed version of the setup I run for a typical NestJS API plus background worker. Note how each path gets the cheapest viable action, and how ignore keeps test files from triggering pointless syncs:
# compose.yaml — NestJS API + worker, watch-enabled
services:
api:
build: ./api
ports:
- "3000:3000"
develop:
watch:
# hot-reload source straight into the container
- path: ./api/src
target: /app/src
action: sync
# dependency changes need a real rebuild
- path: ./api/package.json
action: rebuild
# config tweak? just restart the process
- path: ./api/.env.development
action: sync+restart
target: /app/.env.development
worker:
build: ./worker
develop:
watch:
- path: ./worker/src
target: /app/src
action: sync
ignore:
- "**/*.spec.ts"Then start everything with watch enabled:
docker compose up --watch
# or, to run watch without attaching logs:
docker compose watchPro tip: keep using docker compose up --watch (one terminal, logs and watch together) rather than a separate watch session, until your stack grows. With more than three or four services, split logs into another terminal with docker compose logs -f api so sync notifications do not drown your application output.
I still get asked why not just mount the source directory and call it a day. Here is the honest comparison after running both approaches across client projects and my own VPS-deployed apps:
Compose Watch
One-way host-to-container sync. node_modules stays the container's own, built for Linux inside the image. Editor temp files are filtered automatically, and rapid saves are debounced into batched transfers. Dev environment matches production image behavior closely.
Bind mounts
Two-way by default, so container writes pollute your host tree. Host node_modules shadows the image's unless you add anonymous volume hacks. Slower I/O through the macOS/Windows VM boundary. Still fine for quick experiments and single-service projects.
My rule of thumb: bind mounts for throwaway prototyping, Compose Watch for anything with a Dockerfile that will eventually ship. The sync action gives you bind-mount-level feedback speed while preserving the image as the contract, and that contract is what makes the jump from laptop to my Docker Swarm production nodes boring, which is the goal.
Do not point a sync action at package.json thinking it will install dependencies. Sync only copies files; node_modules in the container will not change. Dependency manifests belong under the rebuild action, full stop.
Compose Watch is one of those quality-of-life features that quietly pays for itself within the first hour. My measured loop on a mid-size NestJS codebase went from roughly 45 seconds per change with rebuilds to under two seconds with sync plus webpack HMR, and unlike bind mounts I did not trade away environment fidelity to get there.
If your team still rebuilds images on every save, or fights node_modules ghosts from bind mounts, spend the fifteen minutes. The configuration surface is three actions and an ignore list. That is the whole feature, and it is enough.
Sources and further reading