Docker Compose is one of those tools that everyone uses but few truly master.
Most developers learn the basics — docker compose up, docker compose down — and stop there. But there's a whole layer of tricks that can transform your local development experience.
Here are the ones I use daily.
1. depends_on With Health Checks (Stop Guessing If Your DB Is Ready)
Ever had your app crash because it started before the database was ready?
Most people do this:
services:
app:
depends_on:
- db
But depends_on only waits for the container to START, not for the service to be READY. Your PostgreSQL might still be initializing when your app tries to connect.
The fix:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
app:
build: .
depends_on:
db:
condition: service_healthy
Now your app waits until PostgreSQL is actually accepting connections. No more race conditions. No more restart loops.
2. Override Files (Dev vs Prod Without Duplicating Everything)
Stop maintaining two separate compose files. Use override files instead.
docker-compose.yml — your base config:
services:
app:
image: myapp:latest
environment:
- NODE_ENV=production
docker-compose.override.yml — automatically loaded in dev:
services:
app:
build: .
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DEBUG=true
ports:
- "3000:3000"
When you run docker compose up, it automatically merges both files. In production, use docker compose -f docker-compose.yml up to skip the override.
Clean. No duplication.
3. profiles — Optional Services That Don't Start By Default
Have services you only need sometimes? Like a debug tool, monitoring stack, or seed script?
services:
app:
build: .
ports:
- "3000:3000"
adminer:
image: adminer
profiles: ["debug"]
ports:
- "8080:8080"
mailhog:
image: mailhog/mailhog
profiles: ["debug"]
ports:
- "8025:8025"
seed:
build: .
profiles: ["setup"]
command: npm run seed
# Normal start — only app runs
docker compose up
# Start with debug tools
docker compose --profile debug up
# Run seed script once
docker compose --profile setup run seed
No more commenting/uncommenting services.
4. watch — Auto-Rebuild Without Volume Mounts
Docker Compose 2.22+ introduced watch mode. It's like hot reload, but for containers:
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
docker compose watch
- Change a source file → synced instantly (no rebuild)
- Change
package.json→ full rebuild triggered automatically
This is way better than volume mounts for most cases — no file permission issues, no node_modules conflicts, no performance problems on Mac.
5. Named Volumes for Dependencies (The node_modules Trick)
The classic Node.js Docker headache: volume mounting overwrites node_modules.
services:
app:
build: .
volumes:
- .:/app
- app_modules:/app/node_modules # Persist separately!
volumes:
app_modules:
This mounts your source code for live editing but keeps node_modules in a separate Docker volume. No more "module not found" errors.
6. .env File Magic
Stop hardcoding values. Use .env files:
.env:
POSTGRES_VERSION=16
NODE_VERSION=22
APP_PORT=3000
docker-compose.yml:
services:
db:
image: postgres:${POSTGRES_VERSION}
app:
build:
args:
NODE_VERSION: ${NODE_VERSION}
ports:
- "${APP_PORT}:3000"
Team members can have different .env files without touching the compose file. Add .env to .gitignore and provide a .env.example.
7. One-Off Commands (Stop Creating Throwaway Containers)
Need to run a migration, open a shell, or execute a one-time script?
# Run a command in a NEW container (removed after)
docker compose run --rm app npm run migrate
# Run a command in an EXISTING container
docker compose exec app sh
# Run with different env vars
docker compose run --rm -e DEBUG=true app npm test
The --rm flag is key — it removes the container after the command finishes. No container graveyard.
8. Resource Limits (Don't Let Containers Eat Your RAM)
services:
app:
build: .
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
Especially important when running multiple services locally. Without limits, one hungry container can freeze your entire machine.
9. Custom Networks (Isolate Your Stacks)
Running multiple projects? They can collide on the default network.
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend
nginx:
networks:
- frontend
networks:
frontend:
backend:
Now nginx can reach app but not db directly. Mimics production network topology.
The Cheat Sheet
| Trick | What It Solves |
|---|---|
Health check + condition
|
App starting before DB is ready |
| Override files | Dev/prod config without duplication |
| Profiles | Optional services cluttering your stack |
| Watch mode | Hot reload without volume mount issues |
| Named volumes | node_modules getting overwritten |
.env files |
Hardcoded values in compose |
run --rm |
Container graveyard from one-off commands |
| Resource limits | Containers eating all your RAM |
| Custom networks | Service isolation |
What's your favorite Docker Compose trick? I'm always discovering new ones — share yours in the comments!
Top comments (0)