Docker Compose Watch: Hot Reload for Your Development Workflow
Use Docker Compose Watch to automatically sync files and rebuild containers during development. Say goodbye to manual rebuilds.
Table of Contents
- What Docker Compose Watch Actually Does
- Why It Matters
- The Bind Mount Problem
- The Developer Experience
- Basic Configuration
- Language-Specific Examples
- Node.js with Express
- Python with FastAPI
- Go with Air
- Multi-Service Stack
- Comparison to Alternatives
- nodemon / Air / watchdog
- Bind Mounts
- Proxmox/LXC Container Considerations
- File System Events Work
- Privileged Mode Required
- Performance Considerations
- Storage Driver
- Tips and Best Practices
- Always Set initial_sync
- Be Specific with Paths
- Separate Dependency Rebuilds
- Handle Frontend Frameworks
- Limitations to Know
- Conclusion
If you’ve spent any time developing with Docker, you know the rhythm: make a code change, rebuild the container, wait 30-60 seconds, restart services, test. Repeat endlessly. It’s the “rebuild hell” that makes containerized development feel slower than running code locally.
Docker Compose Watch, which reached GA status in 2025 after being introduced in v2.20, finally solves this. It syncs your changes into running containers in real-time — no manual rebuilds needed. Let’s look at how it works and why it belongs in your workflow.
What Docker Compose Watch Actually Does
The feature monitors your local filesystem and performs three distinct actions when files change:
- sync — Copies changed files directly into the running container. Instant (1-2 seconds). Best for application code where your framework handles hot reload.
- sync+restart — Syncs files and restarts the main process (not the whole container). Takes 5-10 seconds. Perfect for configuration files.
- rebuild — Triggers a full container rebuild. The slow option (30-60 seconds), but necessary when dependencies change.
The key insight: most code changes don’t need a rebuild. You’re editing application logic, not touching package.json or requirements.txt. Docker Compose Watch recognizes this and gives you instant syncs for the common case while still handling dependency updates when they happen.
Why It Matters
The Bind Mount Problem
Traditional Docker development uses bind mounts (volumes: - ./:/app) to sync code. This works poorly at scale:
- node_modules conflicts — The container’s
node_modules/gets overwritten by your host’s, causing platform incompatibilities (Linux container vs macOS host) - Permission issues — Files created in the container are owned by root; files on host are owned by you
- I/O overhead — File system calls cross VM boundaries on macOS/Windows Docker Desktop
Docker Compose Watch sidesteps all of this by copying files into the container (not mounting), with proper container user permissions.
The Developer Experience
Before watch, a typical workflow looked like:
# Edit file
vim src/api/handlers.go
# Rebuild (30-60s of waiting)
docker compose build api
# Restart
docker compose up -d api
# Test
curl localhost:8080/endpoint
With watch:
# Start once with watch enabled
docker compose up --watch
# Edit file, save — changes sync automatically
vim src/api/handlers.go
# Framework auto-reloads, test immediately
curl localhost:8080/endpoint
No waiting. No context switching. The feature runs in the background while you work.
Basic Configuration
Add a develop section to your compose.yaml:
services:
app:
build: .
ports:
- "3000:3000"
develop:
watch:
- action: sync
path: ./src
target: /app/src
That’s the minimal setup. Run with:
# Watch mode only (see sync activity)
docker compose watch
# Or combine with regular up (logs + watch)
docker compose up --watch
Language-Specific Examples
Node.js with Express
Node.js has two hot reload options: nodemon (established, reliable) and Node’s native --watch flag (newer, v22+). Use nodemon — it’s battle-tested and works reliably with Docker Compose Watch.
compose.yaml:
services:
api:
build: .
ports:
- "3000:3000"
environment:
NODE_ENV: development
command: nodemon server.js
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- node_modules/
initial_sync: true
- action: rebuild
path: package.json
Dockerfile:
FROM node:20-alpine
RUN useradd -ms /bin/sh -u 1001 app
USER app
WORKDIR /app
RUN npm install -g nodemon
COPY package*.json ./
RUN npm install
COPY --chown=app:app . /app
The initial_sync: true ensures your container has fresh code before watching starts — critical for consistent development environments.
Python with FastAPI
Python frameworks like FastAPI and Flask include built-in reload support via Uvicorn and Werkzeug. Combine with Docker Compose Watch for seamless iteration.
compose.yaml:
services:
api:
build: .
ports:
- "8000:8000"
command: uvicorn main:app --host 0.0.0.0 --reload
develop:
watch:
- action: sync
path: ./
target: /app
ignore:
- __pycache__/
- "*.pyc"
- action: rebuild
path: requirements.txt
Dockerfile:
FROM python:3.12-slim
WORKDIR /app
RUN pip install "uvicorn[standard]" fastapi
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"]
Uvicorn’s --reload flag watches for Python file changes inside the container. Docker Compose Watch syncs your host changes into the container. They work together without conflict.
Go with Air
Go requires compilation, which means you need a rebuild step. The rebuild action works, but it’s slow. Better approach: combine Docker Compose Watch with Air, the popular Go hot reload tool.
compose.yaml:
services:
app:
build:
context: .
target: builder
ports:
- "8080:8080"
develop:
watch:
- action: sync
path: ./
target: /app
ignore:
- tmp/
- action: rebuild
path: go.mod
Dockerfile (multi-stage):
# Development stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN go install github.com/air-verse/air@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["air", "-c", ".air.toml"]
.air.toml:
root = "."
tmp_dir = "tmp"
[build]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor"]
include_ext = ["go", "tpl", "tmpl", "html"]
Air handles the build-and-restart cycle inside the container. Docker Compose Watch handles getting your Go files into the container. Together they provide <3 second iterations.
Multi-Service Stack
Real projects have multiple services. Here’s a full-stack setup:
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- WATCHPACK_POLLING=true
develop:
watch:
- action: sync
path: ./frontend/src
target: /app/src
ignore:
- node_modules/
- action: rebuild
path: ./frontend/package.json
api:
build: ./api
ports:
- "8000:8000"
develop:
watch:
- action: sync
path: ./api/src
target: /app/src
initial_sync: true
- action: rebuild
path: ./api/package.json
worker:
build: ./worker
develop:
watch:
- action: sync+restart
path: ./worker/src
target: /app/src
Run docker compose up --watch and all three services sync simultaneously.
Comparison to Alternatives
nodemon / Air / watchdog
These tools still have a place — they handle framework-specific reload logic. Docker Compose Watch doesn’t replace them; it replaces the file synchronization layer.
| Tool | What It Does | Docker Compose Watch |
|---|---|---|
| nodemon | Restarts Node process on file changes | Works together — watch syncs files, nodemon restarts |
| Air | Rebuilds and restarts Go apps | Works together — watch syncs files, Air rebuilds |
| entr | External file watcher, runs arbitrary commands | Replaced by watch (native, no external dependency) |
| watchdog | Python file watching CLI | Works together or use —reload flags |
The combination is powerful: Docker Compose Watch handles container file sync, your framework’s tool handles process restart.
Bind Mounts
Docker Compose Watch wins on three fronts:
| Aspect | Bind Mounts | Watch |
|---|---|---|
| node_modules | Conflicts (host overwrites container) | Clean — only syncs what you specify |
| Performance | I/O overhead crossing VM boundary | Direct file copy, native events |
| Permissions | Root-owned files in container | Uses container’s user, no chown needed |
Proxmox/LXC Container Considerations
If you’re running Docker inside Proxmox LXC containers (a common setup for homelabs), there are a few things to know:
File System Events Work
Docker Compose Watch uses inotify for file system events. LXC containers have native inotify support — no polling required. This works out of the box on modern Proxmox setups (kernel 6.x).
Privileged Mode Required
Your LXC container needs to run Docker, which requires:
# In Proxmox, edit the container config
# /etc/pve/lxc/CTID.conf
lxc.cgroup2.devices.allow: a
lxc.cap.drop:
lxc.apparmor.profile: unconfined
Without this, Docker inside LXC can’t start containers. Watch itself doesn’t add any additional requirements beyond what Docker needs.
Performance Considerations
For large projects with many files (e.g., monorepos with 50k+ files in node_modules), watch performs better than bind mounts. The reason: Docker Compose Watch uses stat, mkdir, rmdir for targeted file operations, while bind mounts create a full virtual filesystem with significant syscall overhead.
Storage Driver
Use overlay2 as your Docker storage driver in LXC. It’s the default on modern setups and works correctly with watch’s file synchronization.
# Check your storage driver
docker info | grep "Storage Driver"
Tips and Best Practices
Always Set initial_sync
- action: sync
path: ./src
target: /app/src
initial_sync: true # Don't skip this
Without initial_sync: true, your container might have stale code on first start. The feature was added in September 2025 specifically to solve this — use it.
Be Specific with Paths
Don’t sync your entire project root:
# Wrong — syncs everything including node_modules
- action: sync
path: ./
target: /app
# Right — sync only what changes
- action: sync
path: ./src
target: /app/src
ignore:
- node_modules/
- "*.test.js"
Separate Dependency Rebuilds
Only trigger rebuilds when dependencies change:
- action: rebuild
path: package.json
- action: rebuild
path: package-lock.json
This avoids unnecessary 30-60 second waits when you’re just editing application code.
Handle Frontend Frameworks
React, Vite, and Next.js use file watchers that need polling inside Docker Desktop:
environment:
- WATCHPACK_POLLING=true # Webpack-based (Next.js)
- CHOKIDAR_USEPOLLING=true # Vite, Create React App
This enables the framework’s hot reload to detect changes synced by Docker Compose Watch.
Limitations to Know
-
Glob patterns don’t work —
path: "./src/**/*.js"isn’t supported. Use directory paths withignorepatterns instead. -
Node.js
--watchflag has issues — The native Node v22+ watch flag doesn’t reliably detect syncs. Use nodemon instead. -
Ignore patterns are relative to path — If you specify
path: ./src, thenignore: ["*.test.js"]ignores test files insidesrc/, not the project root. -
Requires write permissions — Your container’s
USERmust have write access to target directories. UseCOPY --chownin your Dockerfile.
Conclusion
Docker Compose Watch eliminates the biggest friction point in containerized development: waiting for rebuilds. Combined with framework-native hot reload (nodemon, uvicorn —reload, Air), you get instant iterations without leaving Docker.
The setup is straightforward: add a develop section to your compose.yaml, use sync for code and rebuild for dependencies, and run with docker compose up --watch. For homelab setups running Docker in Proxmox LXC containers, watch works without additional configuration — just ensure privileged mode for Docker itself.
After a few weeks with watch, you’ll wonder how you ever lived without it. The mental overhead of “edit-build-wait-restart” disappears, and containerized development finally matches the speed of local development.
Start with your most active project. Add the develop section. Run docker compose watch. Then go write code and forget about rebuilding.
Comments
Powered by GitHub Discussions