Skip to content
shellhacks.dev
Docker

How to Use Compose Watch for Live Reload Without Surprises

Use Docker Compose watch for live reload: configure sync, rebuild and sync restart actions, ignore noisy paths and know when a bind mount is still better.

6 min read
Summarize in AI
Illustration for "How to Use Compose Watch for Live Reload Without Surprises": a terminal session, a disk usage gauge, a directory tree.

The classic development setup bind-mounts the source into the container, which works and brings the permission and speed problems described in bind mount permissions. Compose Watch is the alternative: the container has its own copy, and Compose keeps it up to date. It also solves the case where a dependency change needs a rebuild, which a bind mount cannot do. The commands assume Compose 2.30 and a Node 22 service.

Configure the actions

compose.yamlYAML
services:
  web:
    build: .
    ports:
      - "127.0.0.1:3000:3000"
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - "**/*.test.js"
        - action: sync+restart
          path: ./config
          target: /app/config
        - action: rebuild
          path: package.json

Each rule watches a host path. sync is for code that a dev server reloads by itself, such as a framework with hot module replacement. sync+restart suits configuration that is read only at start. rebuild is for changes that alter the image, usually the dependency manifest or the Dockerfile. Order matters little, since each rule has its own path.

Run it

Shell
docker compose up -d --build
docker compose watch
Text
Watch enabled
Syncing "web" after changes were detected
Rebuilding service "web" after changes were detected...

Edit a file under src and the sync line appears within a moment. Edit package.json and the service rebuilds, with the layer cache making that quick if the Dockerfile copies the manifest before the source. The shortcut docker compose up --watch starts the stack and the watcher together. Stop with Ctrl-C; the containers keep running.

Keep the build cache effective

DockerfileDockerfile
FROM node:22
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]

Copying the manifest before the source means that a rebuild triggered by package.json reinstalls dependencies, while a source change alone never reaches that layer. Add a .dockerignore with node_modules, so the host's copy is not sent in the build context.

Ignore noisy paths

Editors write swap files and build tools emit output, and each such change would sync. Use ignore for those patterns, and Compose also honors .dockerignore entries. Do not put the container's installed dependencies under a synced path, because a sync would overwrite them with the host's, possibly built for another platform.

Where this does not apply

A bind mount still wins when you edit very large trees with many tiny changes or when the tool inside needs the real filesystem, such as a language server. Watch has a small delay and copies files rather than sharing them, so file deletion handling is by event and can lag. It needs a build section, so a service that uses only an image is not watched. See the baseline in the Compose tutorial and variations in profiles and overrides.

What watch does that a bind mount does not

Compose Watch monitors files on the host and, depending on the rule, synchronizes them into the running container, rebuilds the image, or restarts the service. Unlike a bind mount of the source directory, the container keeps its own copy of the files, built from the image, and watch pushes changes into it. That avoids the performance cost of bind mounts on macOS and Windows, where file sharing across the virtual machine boundary is slow, and avoids ownership problems because the files are written by the daemon. It also keeps dependency folders inside the image, so a host with a different operating system does not overwrite native packages compiled for Linux. The price is that you must describe, for each path, what should happen when it changes.

Start it with docker compose watch, or with up --watch to run the stack and the watcher together. The feature needs a recent Compose, and the develop.watch section sits inside each service.

Choosing between sync, rebuild and restart

There are three actions and they match three kinds of change. sync copies changed files into the container, ideal for source code in a language with a reloader such as a Node server in watch mode or a Python server started with a reload flag, so edits take effect in a moment. rebuild rebuilds the image and recreates the container, which is right for changes to dependency manifests such as a package.json or requirements.txt, since the installed packages must change. sync+restart copies the files and restarts the service, for configuration that is read only at startup, and sync+exec runs a command after syncing, for example to run a migration or a build. Pick the cheapest action that is correct for each path, because a rebuild takes seconds to minutes and a sync takes milliseconds.

Making rebuilds cheap and noise free

A rebuild is only fast if the image is layered with change in mind. Copy the dependency manifest and install first, then copy the source, so editing source does not invalidate the install layer. A .dockerignore file keeps the dependency folder, build output and version-control data out of the build context, and the ignore list in each watch rule excludes paths from syncing, such as node_modules and temporary files created by editors. Without the ignore rules, a sync can overwrite container-built packages with host copies, or trigger on every swap file an editor writes. Keep the target path in the container aligned with the working directory in the Dockerfile, since a mismatch is the usual cause of a saved file that never shows up in the running container.

Watch is for development. It adds a file watcher and synchronization that production never needs, so keep the develop section in the override file, and let the base file stay what runs in production.

When watch is the wrong tool

Watch suits applications where an edit can be pushed into a container and picked up by a process that reloads. It does not help when the change needs a full build step that takes minutes, or when the process cannot reload and a restart loses important state. For a compiled language, a rebuild action is the correct choice and its speed depends on how well the layers cache. For a front end with its own hot reload server, a bind mount of the source with the dev server inside the container is often smoother, since the framework handles the update in the browser without a restart. Choose the approach that matches how fast feedback arrives in your stack, and measure how long a typical edit takes to appear.

Debugging also differs. Attaching a debugger to a container needs its port published and the source paths mapped, and a sync that overwrites files while a debugger holds breakpoints can confuse it. Run with the same flags a teammate would use, so a problem in your setup does not become a problem for the whole team.

See using Compose Watch and the develop specification.

Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.