If you're still running services manually during development, we need to talk.
Docker Compose changed how I work. But it took me embarrassingly long to discover the features that actually matter. Not the basics — the tricks that make your local dev environment feel like production without the headaches.
Here are the ones I wish someone told me about sooner.
1. Use profiles to Organize Optional Services
Not every service needs to run all the time. Maybe you only need Redis when testing caching, or you only spin up Mailhog when working on email features.
services:
app:
build: .
ports:
- "3000:3000"
redis:
image: redis:7-alpine
profiles: ["cache"]
mailhog:
image: mailhog/mailhog
profiles: ["email"]
ports:
- "8025:8025"
prometheus:
image: prom/prometheus
profiles: ["monitoring"]
# Only start the app
docker compose up
# Start app + Redis
docker compose --profile cache up
# Start everything
docker compose --profile cache --profile email --profile monitoring up
No more commenting/uncommenting services in your compose file.
2. depends_on with Health Checks (Stop Guessing)
Ever had your app crash because it started before the database was ready? This fixes that:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
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 Postgres is actually accepting connections — not just until the container exists.
3. Hot Reload with watch (Compose 2.22+)
Forget bind mounts for development. Docker Compose now has built-in file watching:
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
docker compose watch
- sync — file changes are synced instantly (like hot reload)
- rebuild — container rebuilds when these files change (like when you add a new dependency)
No more docker compose up --build every time you change a file.
4. Override Files for Dev vs Production
Keep your base config clean. Use override files for environment-specific settings:
├── docker-compose.yml # Base (shared)
├── docker-compose.override.yml # Dev (auto-loaded)
├── docker-compose.prod.yml # Production
docker-compose.yml (base):
services:
app:
image: myapp:latest
environment:
NODE_ENV: production
docker-compose.override.yml (dev — auto-loaded):
services:
app:
build: .
volumes:
- ./src:/app/src
environment:
NODE_ENV: development
DEBUG: "true"
ports:
- "9229:9229" # Node debugger
# Dev (auto-loads override)
docker compose up
# Production (skip override)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
5. Shared Configs with extends and Anchors
Stop repeating yourself. Use YAML anchors for shared config:
x-common: &common
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
api:
<<: *common
build: ./api
ports:
- "3000:3000"
worker:
<<: *common
build: ./worker
command: npm run worker
Both services get the same restart policy and logging config. Change it in one place, applies everywhere.
6. .env Files (Keep Secrets Out of Compose)
Never hardcode credentials in your compose file:
.env:
POSTGRES_PASSWORD=supersecret
REDIS_URL=redis://redis:6379
API_KEY=sk-abc123
docker-compose.yml:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
app:
build: .
env_file:
- .env
Add .env to .gitignore. Share a .env.example with dummy values instead.
7. One-off Commands Without Starting Everything
Need to run a migration, seed data, or debug something?
# Run a one-off command
docker compose run --rm app npm run migrate
# Open a shell in the app container
docker compose exec app sh
# Run database backup
docker compose exec db pg_dump -U postgres mydb > backup.sql
The --rm flag removes the container after it finishes. Clean and simple.
8. Named Volumes for Persistent Data
Don't lose your database every time you docker compose down:
services:
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
# This preserves your data:
docker compose down
# This deletes everything (including volumes):
docker compose down -v
Know the difference. It will save you hours of "why is my data gone?"
My Complete Dev Setup
Here's a real-world compose file I use daily:
services:
app:
build:
context: .
target: development
ports:
- "3000:3000"
- "9229:9229"
volumes:
- ./src:/app/src
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/myapp
REDIS_URL: redis://redis:6379
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
One docker compose up and I have my entire stack running. Every time. On any machine.
TL;DR
| Trick | What It Does |
|---|---|
profiles |
Organize optional services |
depends_on + healthcheck |
Wait for real readiness |
watch |
Built-in hot reload |
| Override files | Dev vs prod configs |
| YAML anchors | DRY shared config |
.env files |
Keep secrets safe |
run --rm |
One-off commands |
| Named volumes | Persistent data |
Docker Compose isn't just for spinning up containers. When used right, it replaces your entire local dev setup script.
What's your favorite Docker Compose trick? I'm always looking for ways to improve my setup — share yours in the comments!
Top comments (0)