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.

• 8 min read
dockerdevelopmenthot-reloadproductivity
Docker Compose Watch: Hot Reload for Your Development Workflow

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.

ToolWhat It DoesDocker Compose Watch
nodemonRestarts Node process on file changesWorks together — watch syncs files, nodemon restarts
AirRebuilds and restarts Go appsWorks together — watch syncs files, Air rebuilds
entrExternal file watcher, runs arbitrary commandsReplaced by watch (native, no external dependency)
watchdogPython file watching CLIWorks 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:

AspectBind MountsWatch
node_modulesConflicts (host overwrites container)Clean — only syncs what you specify
PerformanceI/O overhead crossing VM boundaryDirect file copy, native events
PermissionsRoot-owned files in containerUses 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

  1. Glob patterns don’t workpath: "./src/**/*.js" isn’t supported. Use directory paths with ignore patterns instead.

  2. Node.js --watch flag has issues — The native Node v22+ watch flag doesn’t reliably detect syncs. Use nodemon instead.

  3. Ignore patterns are relative to path — If you specify path: ./src, then ignore: ["*.test.js"] ignores test files inside src/, not the project root.

  4. Requires write permissions — Your container’s USER must have write access to target directories. Use COPY --chown in 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.

Anthony Lattanzio

Anthony Lattanzio

Tech Enthusiast & Builder

I'm a tech enthusiast who loves building things with hardware and software. By night, I run a homelab that's grown way beyond what any reasonable person needs. Check out about me for more.

Comments

Powered by GitHub Discussions