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:
- Add
develop.watchblocks to your services - Use
docker compose up --watchinstead ofup - 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.