background-shape
Compose Watch and Live Reload for Local Development
July 13, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — Compose Watch (v2.6+) is docker compose watch. Define develop.watch actions per service: sync files into container, or rebuild on changes, or restart. Replaces fragile bind-mount + nodemon setups. Especially good for cross-platform (Mac filesystem) performance.

Bind mounts + file-watching dev servers are the standard pattern. They work, but they’re fragile: Mac’s filesystem is slow under heavy I/O, polling-vs-events differs per OS, node_modules synchronization is a nightmare. Compose Watch is Docker’s answer.

What Compose Watch does

A new top-level develop: section per service:

services:
  api:
    build:
      context: .
      target: dev
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./package.json

Run docker compose watch. Compose monitors the paths and reacts:

  • sync — copies changed files into the container without restarting
  • rebuild — rebuilds the image and recreates the container
  • restart — restarts the container (no rebuild)

Apps with their own file watching (nodemon, air for Go, hot-reload tools) react to the synced files normally.

Three actions, three use cases

sync — for code that hot-reloads. Source files copied; dev server inside container picks up the change. Faster than bind mounts because Compose Watch uses native fs events (notify on Linux, fsevents on Mac).

develop:
  watch:
    - action: sync
      path: ./src
      target: /app/src

rebuild — for dep changes. package.json, go.sum, requirements.txt. Container needs to rebuild from scratch.

develop:
  watch:
    - action: rebuild
      path: ./package.json

restart — for config changes. Restart picks up new env, new config files.

develop:
  watch:
    - action: restart
      path: ./config.json

Per-language setups

Node + Vite/Webpack:

services:
  bff:
    build:
      context: ./services/bff
      target: dev
    command: ["npm", "run", "dev"]
    ports: ["3000:3000"]
    develop:
      watch:
        - action: sync
          path: ./services/bff/src
          target: /app/src
        - action: sync
          path: ./services/bff/public
          target: /app/public
        - action: rebuild
          path: ./services/bff/package.json

npm run dev runs Vite. Vite watches /app/src. Compose syncs file changes; Vite hot-reloads them.

Go + air:

services:
  api:
    build:
      context: ./services/api
      target: dev
    command: ["air"]
    ports: ["8080:8080"]
    develop:
      watch:
        - action: sync
          path: ./services/api
          target: /app
        - action: rebuild
          path: ./services/api/go.mod
        - action: rebuild
          path: ./services/api/go.sum

air rebuilds and reruns the binary on .go file changes. Compose syncs; air rebuilds.

Python:

services:
  etl:
    build:
      context: ./services/etl
      target: dev
    command: ["python", "-m", "watchgod", "etl.main.run"]
    develop:
      watch:
        - action: sync
          path: ./services/etl/etl
          target: /app/etl
        - action: rebuild
          path: ./services/etl/poetry.lock

watchgod restarts the process on Python file changes. Same pattern.

Why this beats bind mounts

Three problems bind mounts have:

  1. Mac/Windows fs perf. Mac’s bind mounts have measurable I/O overhead. Reading a node_modules directory through the bind is 5-10× slower than native.
  2. node_modules / vendor synchronization. Bind mount overlays host directory; if host doesn’t have node_modules (or has stale ones), the container’s get clobbered. Anonymous volume hack fixes it but is fragile.
  3. Permissions weirdness. Container user vs host user file ownership conflicts.

Compose Watch sidesteps these. The container has its own filesystem; Compose pushes changes in. Like rsync-from-the-outside.

When bind mounts are still right

Compose Watch isn’t perfect for every case:

  • Database migrations during dev. If you want to edit a SQL migration and have the dev container see it immediately, bind mount is faster.
  • Generated files going OUT of the container. Vite generates HMR sockets; some tools expect to write source maps to the host. Watch is one-way.
  • Tools that expect the source to be a real git directory. Watch syncs file content but not git metadata.

Use bind mounts for these specific cases; Compose Watch for the source code itself.

The watch command

docker compose watch

Brings up the stack and starts watching. Print stream of file changes and actions:

[+] Running 4/4
 ✔ Container my-stack-postgres-1   Started
 ✔ Container my-stack-redis-1      Started
 ✔ Container my-stack-api-1        Started
 ✔ Container my-stack-bff-1        Started

watch enabled

api  → file changed: src/handler.go
       sync action

bff  → file changed: package.json
       rebuild action
       building bff...

Ctrl+C stops watching but leaves containers running.

Performance reality

For a typical Go API:

  • Bind mount + air: ~2.1s rebuild on save
  • Compose Watch sync + air: ~0.4s rebuild on save

For a Node app:

  • Bind mount + Vite HMR: ~600ms hot update
  • Compose Watch sync + Vite HMR: ~150ms hot update

The differences are bigger on Mac than Linux because Mac’s bind-mount overhead is bigger.

Common Pitfalls

Forgetting develop: is separate from volumes:. Don’t mix bind mounts and watch sync on the same path — confusing semantics.

Syncing everything including dist/build output. Pollutes the container with stale builds. Sync source only; rebuild generates fresh artifacts.

Watch on huge directories. Watching node_modules would constantly trigger. Scope path: to source dirs only.

Expecting watch to work without docker compose watch. Regular up doesn’t start watching. Must use the watch subcommand.

Watch + bind mount on the same target. Effectively a race. Compose may sync, then the bind mount overlays. Pick one per path.

No fallback for non-Watch users. Some team members on older Compose; their up works but no live reload. Provide a make watch target that requires v2.6.

Wrapping Up

Compose Watch is the dev-loop improvement of 2022 for containerized local dev. Defines per-service sync/rebuild/restart rules; replaces fragile bind-mount setups. Faster, especially on Mac. Friday: volumes vs bind mounts — the storage primitive these patterns are built on.