Docker Compose Watch: The End of Container Rebuild Hell

Thirty seconds. That’s how long I waited for my Docker container to rebuild after changing a single line of code. Then another thirty seconds. And another. By the end of the day, I’d spent more time watching Docker build images than actually writing code.

Or: How I learned to stop worrying and love file synchronization

If you’ve ever muttered “there has to be a better way” while watching docker compose build churn through layers for the fifteenth time that hour, you’re going to love what Docker shipped in 2025. The watch command is now generally available, and it’s about to transform how you develop with containers.

The Problem: Rebuild Hell

Let’s be honest about what traditional Docker development looks like:

# Edit a file
vim src/Controller/UserController.php

# Rebuild the container
docker compose build
# ⏳ Building... (30-60 seconds)

# Restart
docker compose up
# ⏳ Starting... (10 seconds)

# Test the change
curl localhost:8000/users
# 🤦 Found a typo

# Repeat ad nauseam

We’ve all been there. You’re in the flow, making rapid iterations, and Docker keeps yanking you out with these painful rebuild cycles. Sure, you could mount volumes and hope your application has hot reload. But that’s fragmented, inconsistent across frameworks, and often breaks in subtle ways with file permissions or nested dependencies.

The real kicker? You’re rebuilding entire containers just to copy a few changed files.

Enter Docker Compose Watch

Docker Compose Watch eliminates the rebuild cycle for development. It monitors your source files and syncs changes into running containers in real-time—no rebuilds, no restarts (unless you want them).

As of 2025, it’s production-ready, battle-tested, and ridiculously simple to set up.

The Quick Win

Here’s the minimal setup that’ll save your sanity:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

Start it with:

docker compose watch

That’s it. Change a file in ./src, and Docker instantly syncs it to /app/src in your container. No rebuild. No restart. Just pure, instant feedback.

Two Flavors of Watch

Docker gives you two commands depending on what you want to see:

# Only show watch activity (file syncs, rebuilds)
docker compose watch

# Show everything (watch + all container logs)
docker compose up --watch

I use watch when I’m focused on frontend work and don’t need to see server logs. I use up --watch when debugging full-stack issues where I need to see both file changes and application output.

The Three Watch Actions

Docker Compose Watch supports three synchronization strategies, each optimized for different scenarios.

Action 1: sync (The Fast Path)

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

What it does: Copies changed files directly into the running container.

When to use: Application code that your framework hot-reloads automatically (PHP scripts, Python modules, JavaScript files).

Speed: Instant. Typically 1-2 seconds even for large files.

Gotcha: Your application must detect file changes. This works great with:

  • PHP-FPM (processes files on each request)
  • Node.js with nodemon
  • Python with watchdog
  • Any framework with built-in hot reload

Action 2: rebuild (The Nuclear Option)

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

What it does: Triggers a full container rebuild when the file changes.

When to use: Dependency files or configuration that requires a rebuild (package.json, composer.json, Dockerfile, requirements.txt).

Speed: Same as manual rebuild (30-60 seconds), but automatic.

Why it’s useful: You get instant sync for code changes and automatic rebuilds only when necessary. Best of both worlds.

Action 3: sync+restart (The Middle Ground)

develop:
  watch:
    - action: sync+restart
      path: ./config
      target: /app/config

What it does: Syncs the file and restarts the container’s main process.

When to use: Configuration files that your application only reads on startup (environment configs, service definitions, nginx.conf).

Speed: Fast sync + quick restart (5-10 seconds).

Important: This restarts the process, not the entire container. Much faster than a rebuild.

Real-World Setup: Symfony Application

Let’s build a complete development environment for a Symfony PHP application. This is where Docker Compose Watch really shines.

services:
  php:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/app
    depends_on:
      - database
    develop:
      watch:
        # Sync PHP source code instantly
        - action: sync
          path: ./src
          target: /app/src

        # Sync templates
        - action: sync
          path: ./templates
          target: /app/templates

        # Sync public assets
        - action: sync
          path: ./public
          target: /app/public

        # Rebuild when dependencies change
        - action: rebuild
          path: composer.json

        - action: rebuild
          path: composer.lock

        # Restart when config changes
        - action: sync+restart
          path: ./config
          target: /app/config

  database:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

What this gives you:

  • Edit a controller → instant sync → refresh browser → see changes
  • Edit a template → instant sync → refresh → see changes
  • Edit composer.json → automatic rebuild → container ready with new dependencies
  • Edit config/services.yaml → sync + restart → container reloads configuration

The workflow:

# Start everything with watch enabled
docker compose up --watch

# In another terminal, work normally
vim src/Controller/UserController.php
# Watch logs show: Syncing src/Controller/UserController.php

# Open browser, see changes immediately
# No manual intervention needed

Multi-Service Watch: The Full Stack

Here’s where it gets interesting. You can watch multiple services simultaneously, each with their own sync rules.

services:
  # Backend API (PHP/Symfony)
  api:
    build: ./api
    develop:
      watch:
        - action: sync
          path: ./api/src
          target: /app/src
        - action: rebuild
          path: ./api/composer.json

  # Frontend (React/Vite)
  frontend:
    build: ./frontend
    develop:
      watch:
        - action: sync
          path: ./frontend/src
          target: /app/src
        - action: sync
          path: ./frontend/public
          target: /app/public
        - action: rebuild
          path: ./frontend/package.json

  # Worker (Background jobs)
  worker:
    build: ./worker
    develop:
      watch:
        - action: sync+restart
          path: ./worker/src
          target: /app/src

Run it all:

docker compose up --watch

Now you have:

  • Frontend with Vite hot reload
  • Backend with PHP file watching
  • Worker that restarts on code changes

All synchronized automatically. This is the development experience we deserved all along.

Initial Sync: The September 2025 Addition

Docker added a sneaky-useful feature in September 2025: initial_sync.

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

What it does: Syncs all files immediately when you start docker compose watch, before monitoring for changes.

Why it matters: If you start a container and then start watch, your container might have stale files. With initial_sync: true, Docker ensures everything is synchronized from the start.

When to use: Always. There’s no downside, and it prevents weird “why is my container running old code?” bugs.

Profiles: Environment-Based Services

While we’re modernizing your Docker setup, let’s talk about Compose Profiles. They’re perfect for conditional services based on your environment.

services:
  # Always runs
  app:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

  # Only in development
  mailhog:
    image: mailhog/mailhog
    profiles: ["dev"]
    ports:
      - "8025:8025"

  # Only in development
  adminer:
    image: adminer
    profiles: ["dev"]
    ports:
      - "8080:8080"

  # Only in production-like testing
  monitoring:
    image: prom/prometheus
    profiles: ["prod"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

Development workflow:

docker compose --profile dev up --watch

You get the app, mailhog for email testing, and adminer for database inspection.

Production simulation:

docker compose --profile prod up

You get the app and monitoring, without development tools.

Just the app:

docker compose up

Clean and simple.

The Gotchas (Things I Learned the Hard Way)

File Permissions

Docker’s watch syncs files with the container’s user permissions. If your container runs as www-data but your host files are owned by your user, you might see permission errors.

Solution: Match user IDs in your Dockerfile:

ARG USER_ID=1000
ARG GROUP_ID=1000

RUN groupmod -g ${GROUP_ID} www-data && \
    usermod -u ${USER_ID} -g ${GROUP_ID} www-data

Build with your IDs:

docker compose build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g)

Ignore Patterns

Watch doesn’t have built-in ignore patterns yet. If you’re syncing a directory with node_modules or vendor, you’ll sync everything.

Workaround: Be specific with paths:

develop:
  watch:
    # ❌ Syncs everything including node_modules
    - action: sync
      path: ./
      target: /app

    # ✅ Sync only what you need
    - action: sync
      path: ./src
      target: /app/src
    - action: sync
      path: ./public
      target: /app/public

Volumes vs Watch

If you already have volumes mounted, watch might seem redundant. The difference:

Volume mount: Bidirectional, persistent, shares the same inode Watch sync: One-way (host → container), isolated, safer for node_modules conflicts

You can use both:

services:
  app:
    volumes:
      # Volume for persistent data
      - db_data:/var/lib/mysql
    develop:
      watch:
        # Watch for code changes
        - action: sync
          path: ./src
          target: /app/src

Multiple Services Fixed in February 2025

Early versions of watch had issues with multiple services. If you’re using Docker Compose 2.25 or later (February 2025 release), this is fixed. Multiple services with watch work flawlessly.

If you’re on an older version:

docker compose version

Upgrade if you’re below 2.25.

The Node.js Hot Reload Setup

Here’s a complete Node.js/Express example with true hot reload:

Dockerfile:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

# Install nodemon globally for hot reload
RUN npm install -g nodemon

CMD ["nodemon", "server.js"]

compose.yaml:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

        - action: sync
          path: ./public
          target: /app/public

        - action: rebuild
          path: package.json

The magic: Nodemon detects file changes inside the container and restarts the Node process. Watch syncs files from host to container. Together, they create seamless hot reload.

Start it:

docker compose up --watch

Edit any file in ./src, and nodemon automatically restarts. Zero manual intervention.

Bonus 2025 Features You Should Know

While you’re modernizing your Docker workflow, here are other gems from 2025:

CTRL+Z Background Mode

docker compose up --watch
# Press CTRL+Z
# Compose moves to background, returns to terminal

No more opening another terminal window. This is chef’s kiss.

Compose Bridge

Convert your Docker Compose to Kubernetes:

docker compose bridge convert --format kubernetes

Or generate Helm charts:

docker compose bridge convert --format helm

Useful for staging/production deployments while keeping your local development in Compose.

Bake as Default Builder

For complex builds with multiple platforms:

services:
  app:
    build:
      context: .
      platforms:
        - linux/amd64
        - linux/arm64

Docker now uses BuildKit Bake under the hood, making multi-platform builds significantly faster.

The Transformation

Let’s talk about what this actually means for your day.

Before Docker Compose Watch:

  • Edit file
  • Wait 30-60 seconds for rebuild
  • Wait 10 seconds for restart
  • Test change
  • Repeat 50-100 times per day
  • Time wasted: hours

After Docker Compose Watch:

  • Edit file
  • Wait 1-2 seconds for sync
  • Test change
  • Repeat infinitely
  • Time wasted: minutes

The numbers matter, but the feeling matters more. You stay in flow. You don’t context-switch to check Twitter while waiting for builds. You don’t lose your train of thought.

When NOT to Use Watch

Let’s be honest about the limitations:

Production: Never. Watch is a development tool. Your production containers should be immutable, built once, deployed everywhere.

CI/CD: Nope. Your CI pipeline should build clean images from scratch every time.

Compiled languages with complex builds: If your Go/Rust/Java application requires 5 minutes to compile, watch won’t help with the compilation itself. You still need the build step. But watch can sync source files and trigger rebuilds automatically.

Shared volumes work fine: If you’re happy with volume mounts and they work for your setup, don’t change. Watch shines when volumes create problems (node_modules conflicts, performance on Mac/Windows, permission issues).

The Alternatives (And Why Watch Wins)

Volume Mounts

Pros:

  • Simple, been around forever
  • No special configuration

Cons:

  • Bidirectional (container can modify host files)
  • Performance issues on Mac/Windows
  • node_modules conflicts drive you insane
  • Permission problems

Verdict: Great for simple cases, painful at scale

docker-sync

Pros:

  • Solves Mac/Windows performance
  • Mature ecosystem

Cons:

  • Third-party tool
  • Extra daemon to run
  • Complex configuration

Verdict: Was necessary, now obsolete with watch

Manual rebuild scripts

Pros:

  • Total control

Cons:

  • You’re maintaining custom tooling
  • Error-prone
  • Everyone on the team needs to know your setup

Verdict: Stop reinventing the wheel

Docker Compose Watch

Pros:

  • Native Docker feature
  • Zero dependencies
  • Declarative configuration
  • Works across platforms
  • Maintained by Docker, Inc.

Cons:

  • Relatively new (but GA in 2025)
  • No ignore patterns yet

Verdict: The modern standard

Getting Started Today

Here’s your action plan:

1. Upgrade Docker:

docker compose version
# Should be 2.25.0 or higher

2. Add watch to your compose.yaml:

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

3. Start watching:

docker compose watch

4. Edit a file and smile:

Watch the sync happen in 1-2 seconds. Feel the time you’re saving. Wonder why you didn’t do this sooner.

Wrapping Up

Docker Compose Watch is one of those features that seems small on paper but fundamentally changes your daily experience. The end of container rebuild hell isn’t some distant utopia—it’s here, it’s stable, and it’s ridiculously easy to adopt.

Three changes that’ll transform your Docker development:

  1. Add develop.watch blocks to your services
  2. Use docker compose up --watch instead of up
  3. Stop waiting for rebuilds

Try it on your next project. Better yet, add it to an existing project and feel the difference immediately. You’ll wonder how you ever tolerated the old way.

Now if you’ll excuse me, I have some code to write. And for the first time in years, I’m not going to spend half my day watching Docker build images.


Resources:

Got a clever watch configuration or a gotcha I missed? Drop it in the comments. Let’s build better development workflows together.