Fix: Docker Compose Watch Not Working — sync vs rebuild, Ignore Patterns, WSL/macOS File Events
Quick Answer
How to fix docker compose watch errors — develop.watch directive not firing, sync vs sync+restart vs rebuild differences, ignore globs not matching, WSL2 file events delayed, named volumes shadowing watch, and Compose version requirements.
The Error
You add a develop.watch block and docker compose up --watch runs, but file changes don’t propagate:
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/srcYou edit ./src/index.ts and nothing happens in the container.
Or sync+restart triggers a restart loop:
[+] Restarting service "app"
[+] Restarting service "app"
[+] Restarting service "app"Or --watch exits with:
the docker compose version is too old to support watchOr watching works on macOS but not on Windows / WSL2:
# Files saved in VS Code on Windows.
# Container's /app/src is unchanged.Why This Happens
docker compose watch is a developer-experience layer on top of bind mounts. It detects file changes on the host, then performs one of three actions:
sync— copy changed files into the running container’s filesystem.sync+restart— sync, then restart the container (for code that runs once at startup).rebuild— rebuild the image and recreate the container (for Dockerfile or build-context changes).
Three sources of pain:
path/targetmismatch. Thepathis on your host;targetis in the container. A path that doesn’t match a real file location or a target that conflicts with a volume produces silent no-ops.- WSL2 and Docker Desktop file events. Editing files on Windows (
C:\...) while the container runs against a WSL filesystem (or vice versa) means the file change events cross filesystem boundaries. Some configurations don’t deliver inotify events reliably. - Compose version requirements.
develop.watchrequires Compose v2.22+ and a recent Docker Engine. Older versions silently ignore the section or print the “too old” error.
Fix 1: Verify Compose Version
docker compose version
# Docker Compose version v2.27.0 ← need v2.22 or laterIf the version is older, update Docker Desktop (Mac/Windows) or install the latest compose plugin on Linux:
# Linux — install the latest from Docker's apt repo
sudo apt-get update
sudo apt-get install docker-compose-plugindevelop.watch was promoted from experimental in Compose v2.22 (late 2023). Anything older needs an upgrade.
Fix 2: Pick the Right Action for the Change Type
Different file changes need different actions:
services:
app:
build: .
develop:
watch:
# Source code that hot-reloads inside the container (Vite, nodemon, etc.):
- action: sync
path: ./src
target: /app/src
ignore:
- "**/*.test.ts"
- "node_modules/"
# Config files the app reads at startup — sync + restart:
- action: sync+restart
path: ./config
target: /app/config
# Dockerfile changes need a rebuild:
- action: rebuild
path: ./Dockerfile
# Lockfile changes — rebuild to pick up new deps:
- action: rebuild
path: ./package-lock.jsonsync alone is what you want for most app source code if your dev process (Vite, Next.js, nodemon, watchexec) hot-reloads on file changes. sync+restart is for code that runs at startup and doesn’t watch its own files.
Pro Tip: Don’t use rebuild for source files. It blows away the container state and is much slower than sync. Reserve rebuild for Dockerfile, package.json (dep changes), or lockfiles.
Fix 3: Match path to Real Locations
path is a directory or file on the host, relative to the compose file:
develop:
watch:
- action: sync
path: ./src # → ./src/* events
target: /app/src # → copied to /app/src inside the containerFor multiple discrete paths, list them separately:
develop:
watch:
- action: sync
path: ./apps/web/src
target: /app/apps/web/src
- action: sync
path: ./packages/shared/src
target: /app/packages/shared/srcThe compose file’s directory is the base. ./src means “src relative to docker-compose.yml” — not relative to wherever you ran docker compose up.
Common Mistake: Mapping path: ./src to target: /app (no /src). The sync writes files directly into /app, potentially overwriting files outside the source tree. Always make target mirror the source structure inside the container.
Fix 4: Use ignore to Skip Unwanted Triggers
Without an ignore list, Compose watches every file in path, including node_modules/, .git/, build artifacts, and editor temp files. These often trigger thousands of useless sync events:
develop:
watch:
- action: sync
path: ./
target: /app
ignore:
- "node_modules/"
- ".git/"
- "dist/"
- "*.log"
- "**/__pycache__/"
- ".venv/"
- ".pytest_cache/"Patterns use gitignore-style globs. node_modules/ matches at any depth; **/__pycache__/ matches in subdirectories explicitly.
Pro Tip: Mirror your .gitignore. If you cat .gitignore and convert each line to a Compose ignore, you’ll catch nearly every false-positive event.
Fix 5: Avoid Conflicts With Bind Mounts and Volumes
If you already have a bind mount and add develop.watch on the same path, the two mechanisms fight each other:
services:
app:
volumes:
- ./src:/app/src # Bind mount — files are live-shared
develop:
watch:
- action: sync
path: ./src # ALSO sync — redundant, can race
target: /app/srcPick one:
- Bind mount only — fastest, native file sharing. Use when host/container filesystems play nicely.
develop.watch synconly — copies files via Compose. Use when bind mounts are slow (macOS) or the container needs different file ownership.
For named volumes that shadow your watched path:
services:
app:
volumes:
- node_modules:/app/node_modules # Named volume — survives restarts
develop:
watch:
- action: sync
path: ./
target: /app
ignore:
- "node_modules/" # Don't sync node_modulesWithout ignoring node_modules, the watch tries to sync it into the named volume, which doesn’t go well.
Fix 6: WSL2 and File Event Reliability
If you edit files on Windows (C:\code\...) and the container watches a path mounted from \\wsl$\Ubuntu\..., file events can be delayed or dropped. Two safer patterns:
Put the project entirely inside WSL:
# In WSL:
cd ~/code
git clone https://github.com/...
docker compose up --watchThen edit files via VS Code’s Remote-WSL extension or directly inside WSL. File events stay within one filesystem.
On macOS, prefer Compose watch over bind mounts for large repos:
Bind mounts on macOS go through a virtualization layer and are slow for large file trees. develop.watch sync is one-way (host → container) and faster.
Common Mistake: Mounting Windows paths into Linux containers via C:\ instead of /c/. Docker Desktop translates, but file watcher behavior is inconsistent across CLI vs Docker Desktop UI launches. Stick to WSL paths.
Fix 7: Restart Strategy When sync+restart Loops
A restart loop usually means:
- The container exits quickly after restart (check
docker compose logs appfor the actual error). - Your code re-saves a watched file at startup (creates a feedback loop).
- A
chmodorchownduring startup triggers Compose to see a “change” event.
To diagnose:
docker compose logs --tail 50 appLook for the last error before the restart. If the container can’t start (missing env var, crash), sync+restart will keep restarting it.
If the issue is your app re-saving a watched file at startup, add it to ignore:
ignore:
- "src/generated/"
- "src/version.ts" # Auto-regenerated on each startFix 8: Combine With docker compose up --build --watch
For complex changes (deps + source), launch with both --build and --watch:
docker compose up --build --watch--buildrebuilds at startup (catching changes since the last build).--watchkeeps watching after.
For CI-friendly one-off runs without watching:
docker compose up -d
docker compose watch # Detached watch in foregroundOr run watch alongside the existing stack:
docker compose watch app frontend # Watch only specific servicesStill Not Working?
A few less-obvious failures:
watchenabled but nothing logs on file change. Run with--verbose:docker compose up --watch --verbose. Look for[watch]lines confirming the directives were parsed.- Build context too large during
rebuild. Add a.dockerignore. Without it, everyrebuildre-uploadsnode_modules/,.git/, etc. to the daemon. syncsucceeds but the container still serves stale files. Your dev server isn’t watching the synced path. Check that Vite/Next/nodemon’s watch root matches thetarget.- Container has wrong file ownership after sync.
develop.watchcopies as the user the daemon runs as. Setuser:in the compose file orUSERin the Dockerfile to align with the host UID. - macOS: spinning beachball when watching a node_modules-heavy project. Use a named volume for
node_modulesand addnode_modules/toignore. develop.watchfires twice for one save. Editor saves write a temp file, then rename — two events. Add*.tmpand editor-specific patterns toignore.- Apple Silicon (ARM) builds slow. Specify
platform: linux/arm64in your service to avoid emulation.rebuildactions go from minutes to seconds. watchworks butrestartdoesn’t actually restart the process. The container has arestart: nopolicy, and Compose’s restart is in-container. Setrestart: unless-stoppedfor the service, or usecommand:to wrap your process with a supervisor that restarts on signal.
For related Docker development workflow issues, see Docker Compose env not loaded, Docker Compose service failed to build, Docker volume permission denied, and Docker Compose networking not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Docker Compose Services Can't Connect to Each Other
How to fix Docker Compose networking issues — services can't reach each other by hostname, port mapping confusion, network aliases, depends_on timing, and host vs container port differences.
Fix: Docker Container Keeps Restarting
How to fix a Docker container that keeps restarting — reading exit codes, debugging CrashLoopBackOff, fixing entrypoint errors, missing env vars, out-of-memory kills, and restart policy misconfiguration.
Fix: Docker Compose depends_on Not Waiting for Service to Be Ready
How to fix Docker Compose depends_on not working — services start in order but the app still crashes because depends_on only waits for container start, not service readiness. Includes healthcheck solutions.
Fix: Fly.io Deploy Not Working — fly.toml, Machines, Volumes, Secrets, and Internal DNS
How to fix Fly.io errors — fly.toml app vs name confusion, machines API vs legacy apps, Dockerfile build failures, volume per-region, secrets staging, fly proxy for local access, and internal IPv6 routing.