$docker.recipes
·10 min read·Updated February 2026

Docker Compose Watch: Hot Reload for Container Development

Stop running docker compose build after every change. Compose Watch syncs files, rebuilds, and restarts containers automatically based on file changes.

docker-composedevelopmenthot-reloaddevops

01The Inner Loop Problem

The development "inner loop" — the cycle of editing code, building, and testing — is where developers spend most of their time. In a containerized workflow, this loop traditionally looks like: edit code, run docker compose build, run docker compose up, wait, test, repeat. On a good day, each cycle takes 30-60 seconds. On a bad day with complex builds, it's several minutes. This friction is why many developers develop locally and only use Docker for deployment. But that defeats the purpose of containers — your development environment doesn't match production, and "works on my machine" bugs creep in. Docker Compose Watch (introduced in Compose 2.22) solves this by monitoring your source files and automatically syncing, rebuilding, or restarting containers when files change. It's like nodemon or webpack-dev-server, but for Docker Compose. I've been using it for 8 months across Node.js, Python, and Go projects, and it's transformed my container development workflow. The inner loop dropped from 30-60 seconds to 1-3 seconds for most changes.

02How Compose Watch Works

Compose Watch supports three actions, and choosing the right one for each file pattern is key: sync: Copies changed files directly into the running container without rebuilding. This is the fastest option — changes appear in milliseconds. Use it for interpreted languages (JavaScript, Python, Ruby) where the runtime picks up file changes automatically. rebuild: Triggers a full docker compose build and recreates the container. This is the slowest option but necessary when dependencies change (package.json, requirements.txt, go.mod). The rebuild uses cache layers, so only the changed layer and those after it are rebuilt. sync+restart: Syncs the files and then restarts the container. Use this for compiled languages or when configuration files change — the files need to be in the container, and the process needs to restart to pick them up.
[docker-compose.yml]
1services:
2 web:
3 build: .
4 ports:
5 - "3000:3000"
6 develop:
7 watch:
8 # Sync source files — hot reload via nodemon/vite
9 - action: sync
10 path: ./src
11 target: /app/src
12
13 # Rebuild when dependencies change
14 - action: rebuild
15 path: ./package.json
16
17 # Rebuild when Dockerfile changes
18 - action: rebuild
19 path: ./Dockerfile
20
21 # Sync+restart for config files
22 - action: sync+restart
23 path: ./config
24 target: /app/config

03Configuration by Language

Each language has different needs based on whether it's interpreted or compiled. Here are the configurations I use for the languages I work with most.
[docker-compose.yml]
1# Python (Django/Flask) — sync source, rebuild on deps
2services:
3 api:
4 build: ./backend
5 develop:
6 watch:
7 - action: sync
8 path: ./backend/app
9 target: /app/app
10 - action: rebuild
11 path: ./backend/requirements.txt
12 - action: sync+restart
13 path: ./backend/gunicorn.conf.py
14 target: /app/gunicorn.conf.py
15
16# Go — rebuild on any .go change (compiled language)
17 worker:
18 build: ./worker
19 develop:
20 watch:
21 - action: rebuild
22 path: ./worker/
23 # Ignore non-Go files to avoid unnecessary rebuilds
24 ignore:
25 - "**/*.md"
26 - "**/*_test.go"
27
28# PHP (Laravel) — sync everything, restart on config
29 php:
30 build: ./php
31 develop:
32 watch:
33 - action: sync
34 path: ./php/app
35 target: /var/www/html/app
36 - action: sync
37 path: ./php/resources
38 target: /var/www/html/resources
39 - action: sync+restart
40 path: ./php/.env
41 target: /var/www/html/.env

For interpreted languages (Python, Node.js, PHP, Ruby), use sync for source files and let the framework's built-in reload handle the rest. For compiled languages (Go, Rust, Java), use rebuild — there's no shortcut around recompilation. Use multi-stage Docker builds to keep rebuild times short.

04Ignore Patterns and Performance

Without proper ignore patterns, Compose Watch can trigger unnecessary rebuilds or saturate the file sync. Node_modules is the worst offender — a single npm install can trigger thousands of file change events.
[docker-compose.yml]
1services:
2 web:
3 build: .
4 develop:
5 watch:
6 - action: sync
7 path: ./src
8 target: /app/src
9 ignore:
10 - "node_modules/"
11 - "**/*.test.ts"
12 - "**/*.spec.ts"
13 - "**/__tests__/"
14 - "**/.DS_Store"
15 - "**/*.swp"
16 - "**/coverage/"
17 - "**/.git/"
18
19 - action: rebuild
20 path: ./package.json
21
22 - action: rebuild
23 path: ./package-lock.json

In monorepos or projects with many files (10,000+), Compose Watch can cause high CPU usage from filesystem event processing. If you notice your machine slowing down, narrow the watch paths to specific directories rather than watching the entire project root. Watch ./src instead of ./ and add aggressive ignore patterns.

05Watch vs Bind Mounts

Before Compose Watch, the standard approach was bind mounts — mounting your source code directly into the container with volumes: - ./src:/app/src. This works but has problems: Performance: Bind mounts on macOS (Docker Desktop and alternatives) have slower file I/O than native filesystem access. This manifests as slow npm install, slow test runs, and sluggish file watching. Compose Watch's sync action copies files into the container's native filesystem, which is faster. Consistency: With bind mounts, your container sees your host's file system directly, including OS-specific files, different line endings, and permission issues. Sync creates a clean copy. Production parity: Your production containers don't use bind mounts — they copy files during the build. Compose Watch's approach is closer to production behavior. When to still use bind mounts: Real-time file access is needed (database files, live logs you want to tail from the host), or when you need bidirectional sync (generated files that your IDE needs to see). Compose Watch is one-way: host to container only. My recommendation: use Compose Watch for application source code and bind mounts for data that needs to persist or be accessed from the host. They complement each other.

06A Complete Development Workflow

Here's the full Compose file I use for a typical web application project — a Node.js frontend, Python API, and PostgreSQL database. This configuration gives me sub-second hot reload for both the frontend and backend, with automatic rebuilds when dependencies change. Start the stack with: docker compose watch. That's it. Edit a React component — the browser updates in under a second. Edit a Python endpoint — the API restarts in 2 seconds. Update package.json — the container rebuilds automatically. The first time you experience this workflow, the old build-restart-test cycle feels archaic. Combined with OrbStack on macOS (see our Docker alternatives article), the container development experience is finally as smooth as local development — with the added benefit that your environment is reproducible and matches production.
[docker-compose.yml]
1services:
2 frontend:
3 build:
4 context: ./frontend
5 target: development
6 ports:
7 - "3000:3000"
8 environment:
9 - API_URL=http://api:8000
10 develop:
11 watch:
12 - action: sync
13 path: ./frontend/src
14 target: /app/src
15 - action: rebuild
16 path: ./frontend/package.json
17
18 api:
19 build:
20 context: ./backend
21 target: development
22 ports:
23 - "8000:8000"
24 environment:
25 - DATABASE_URL=postgresql://app:secret@db:5432/myapp
26 depends_on:
27 db:
28 condition: service_healthy
29 develop:
30 watch:
31 - action: sync
32 path: ./backend/app
33 target: /app/app
34 - action: rebuild
35 path: ./backend/requirements.txt
36 - action: sync+restart
37 path: ./backend/alembic
38 target: /app/alembic
39
40 db:
41 image: postgres:16-alpine
42 environment:
43 - POSTGRES_DB=myapp
44 - POSTGRES_USER=app
45 - POSTGRES_PASSWORD=secret
46 volumes:
47 - pgdata:/var/lib/postgresql/data
48 healthcheck:
49 test: ["CMD", "pg_isready", "-U", "app"]
50 interval: 5s
51 timeout: 3s
52 retries: 5
53
54volumes:
55 pgdata:

About the Author

Frank Pegasus

DevOps engineer and self-hosting enthusiast with over a decade of experience running containerized workloads in production. Creator of docker.recipes.