Docker Compose Watch: Hot Reload for Containers
Learn how to use Docker Compose Watch to automatically sync file changes and rebuild containers during development. Faster feedback loops without manual restarts.
What you'll learn
- ✓What Docker Compose Watch is and how it works
- ✓Configuring sync, rebuild, and sync+restart actions
- ✓Setting up watch for Node.js, Python, and Go projects
- ✓Comparing watch to bind mounts and other solutions
Prerequisites
- •Docker Compose v2.22 or later
- •Basic Docker Compose knowledge
Developing inside containers has always had one major friction point: the feedback loop. You change a file, then you have to rebuild the image, recreate the container, and wait. Bind mounts helped, but they come with their own problems around file permissions, performance on macOS, and leaking host-specific paths into your configuration. Docker Compose Watch, introduced in Compose v2.22, gives you a better alternative. It watches your source files and automatically syncs changes, restarts services, or triggers rebuilds depending on what changed.
How Compose Watch Works
Compose Watch monitors your host filesystem for changes and takes one of three actions:
- sync: Copies the changed file directly into the running container without restarting it. Best for interpreted languages and static assets.
- rebuild: Triggers a full image rebuild and container replacement. Best for changes to dependency files like
package.jsonorrequirements.txt. - sync+restart: Syncs the file into the container and then restarts the service. Best for compiled languages or configuration files that need a process restart to take effect.
You configure these actions in your compose.yaml under a develop.watch section for each service.
Starting Compose Watch
docker compose watch
This starts all services defined in your compose.yaml and begins watching for file changes. The command runs in the foreground and logs sync/rebuild events as they happen:
watching: app
syncing: src/index.ts -> /app/src/index.ts
syncing: src/routes/api.ts -> /app/src/routes/api.ts
rebuilding: service "app" (package.json changed)
You can also start watch alongside your regular services:
docker compose up --watch
Configuring Watch for Node.js
Here is a typical setup for a Node.js application using TypeScript and nodemon:
services:
app:
build:
context: .
target: development
ports:
- "3000:3000"
environment:
NODE_ENV: development
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
- action: rebuild
path: ./package-lock.json
The corresponding multi-stage Dockerfile:
FROM node:20-alpine AS base
WORKDIR /app
FROM base AS development
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npx", "nodemon", "--watch", "src", "src/index.ts"]
FROM base AS production
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
With this setup:
- Editing any file in
src/syncs it into the container instantly. Nodemon detects the change and restarts the Node process. - Changing
package.jsonorpackage-lock.jsontriggers a full rebuild so new dependencies are installed.
Configuring Watch for Python
For a Python Flask or FastAPI application:
services:
api:
build:
context: .
ports:
- "8000:8000"
develop:
watch:
- action: sync
path: ./app
target: /code/app
- action: rebuild
path: ./requirements.txt
- action: sync+restart
path: ./config.yaml
target: /code/config.yaml
FROM python:3.12-slim
WORKDIR /code
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
Here, --reload in uvicorn handles the process-level restart when Python files change. The sync action just gets the files into the container. For config.yaml, we use sync+restart because the config is only read at startup.
Configuring Watch for Go
Go requires compilation, so the strategy differs:
services:
api:
build:
context: .
target: dev
ports:
- "8080:8080"
develop:
watch:
- action: sync+restart
path: ./cmd
target: /app/cmd
- action: sync+restart
path: ./internal
target: /app/internal
- action: rebuild
path: ./go.mod
- action: rebuild
path: ./go.sum
FROM golang:1.22-alpine AS dev
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"]
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /bin/server /bin/server
ENTRYPOINT ["/bin/server"]
The air tool watches for file changes inside the container and recompiles the Go binary. Compose Watch syncs the changed source files in, and air handles the rebuild and restart.
Ignoring Files
You do not want every file change to trigger a sync. Use the ignore field to exclude paths:
services:
app:
build:
context: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/__tests__/**"
- "**/node_modules/**"
- action: rebuild
path: ./package.json
The ignore patterns use standard glob syntax. This prevents test file edits from triggering unnecessary syncs that could slow down feedback.
Watch with Multiple Services
A typical full-stack application has a frontend, backend, and database:
services:
frontend:
build:
context: ./frontend
ports:
- "5173:5173"
develop:
watch:
- action: sync
path: ./frontend/src
target: /app/src
- action: rebuild
path: ./frontend/package.json
backend:
build:
context: ./backend
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
develop:
watch:
- action: sync
path: ./backend/src
target: /app/src
- action: rebuild
path: ./backend/package.json
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Running docker compose watch monitors both frontend and backend source directories simultaneously. The database service has no watch configuration because it uses a pre-built image.
Watch vs Bind Mounts
You might wonder why not just use bind mounts like before:
# The old way - bind mounts
services:
app:
build: .
volumes:
- ./src:/app/src
Bind mounts have real drawbacks:
- Performance on macOS and Windows: File system events cross the VM boundary, causing significant latency. On large projects, this can make your application noticeably slow.
- Permission issues: The UID/GID inside the container may not match your host user, leading to permission denied errors or files owned by root on your host.
- Host path leakage: Your
compose.yamlcontains absolute or relative host paths that differ across team members’ machines. - No selective action: Every file change is immediately visible. You cannot trigger a rebuild for
package.jsonwhile syncingsrc/files.
Compose Watch solves all of these. It copies files using a high-performance sync mechanism, handles permissions internally, and lets you define different actions for different file patterns.
Debugging Watch Issues
Files Not Syncing
Check that your path matches the actual location:
# Verify the watch configuration
docker compose config | grep -A 10 watch
# Check container filesystem
docker compose exec app ls -la /app/src
Rebuild Loops
If a rebuild triggers file changes that cause another rebuild, add the generated files to the ignore list:
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- "**/dist/**"
- "**/.cache/**"
Slow Syncs
For large projects, limit the watch scope to only the directories that matter:
# Instead of watching the entire project
- action: sync
path: .
target: /app
# Watch specific directories
- action: sync
path: ./src
target: /app/src
- action: sync
path: ./public
target: /app/public
Performance Tips
- Keep watch paths narrow. Watching
./srcis faster than watching.because there are fewer files to monitor. - Use ignore patterns aggressively. Exclude test files, build outputs, and IDE-specific directories.
- Prefer sync over rebuild. Rebuilds are slow because they rebuild the entire image. Only use rebuild for files that truly require it, like dependency manifests.
- Combine with in-container watchers. Use nodemon, uvicorn reload, or air inside the container to handle the process restart after sync delivers the files. This is faster than
sync+restartwhich restarts the entire container.
Wrapping Up
Docker Compose Watch brings hot-reload ergonomics to containerized development without the downsides of bind mounts. Configure sync for source files, rebuild for dependency changes, and sync+restart for configuration files. Pair it with in-container watchers like nodemon or air for the fastest feedback loop. The setup takes a few minutes to add to your compose.yaml and eliminates the manual rebuild cycle that slows down container-based development.
Related articles
- Docker Docker Compose Patterns for Production
Production-ready Docker Compose patterns: override files, healthchecks, resource limits, logging, and secrets management.
- Docker Docker Compose Network Aliases Tutorial
Network aliases let containers reach each other under multiple names. Learn how aliases work in Compose, when to use them, and the gotchas to avoid.
- Docker Docker vs Podman: Which Container Runtime to Use
Compare Docker and Podman on architecture, security, compatibility, and developer experience. Learn which container runtime fits your workflow in 2026.
- Docker Docker Init: Generate Dockerfiles Automatically
Learn how to use docker init to scaffold Dockerfiles, Compose files, and .dockerignore for your projects in seconds. Supports Node.js, Python, Go, Rust, and more.