The biggest source of “works on my machine” bugs is environment differences between developers and production. Docker Compose solves this by defining your entire stack — API, database, Redis, message queue — as code. One docker compose up gives every developer and every CI run the same environment.
⚡ TL;DR: Define each service, its image, environment variables, ports, and volumes in compose.yml. Use depends_on with health checks to start services in order. Use named volumes for persistent data. Use profiles for optional services. Override with compose.override.yml for local tweaks.
A complete development stack
# compose.yml
services:
api:
build: . # Build from Dockerfile in current dir
ports: ['3000:3000']
environment:
DATABASE_URL: postgres://user:pass@db:5432/myapp
REDIS_URL: redis://cache:6379
NODE_ENV: development
volumes:
- .:/app # Hot reload: mount source into container
- /app/node_modules # Anonymous volume: keep container's node_modules
depends_on:
db:
condition: service_healthy # Wait for DB health check to pass
cache:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- pg_data:/var/lib/postgresql/data # Named volume: persists between restarts
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # Auto-run SQL on first start
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U user -d myapp']
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports: ['6379:6379']
worker:
build: .
command: npm run worker
environment:
DATABASE_URL: postgres://user:pass@db:5432/myapp
depends_on: [db, cache]
profiles: [worker] # Only starts with: docker compose --profile worker up
volumes:
pg_data:
Essential compose commands
# Start everything (build if needed)
docker compose up --build
# Start in background
docker compose up -d
# View logs
docker compose logs -f api # Follow API logs
docker compose logs -f # All services
# Shell into running container
docker compose exec api sh
docker compose exec db psql -U user myapp
# Run one-off command
docker compose run --rm api npm run migrate
# Stop and remove containers (keeps volumes)
docker compose down
# Stop and remove everything including volumes (DELETES DATA)
docker compose down -v
# Rebuild just one service
docker compose up -d --build api
Override files for local customization
# compose.override.yml (auto-loaded by compose, git-ignored)
# Override production settings for local dev
services:
api:
environment:
DEBUG: 'true'
LOG_LEVEL: debug
volumes:
- .:/app # Extra volume for hot reload
# Use separate files for different environments:
docker compose -f compose.yml -f compose.prod.yml up
- ✅ Always add healthcheck to DB and cache services — api starts after they are ready
- ✅ Named volumes for persistent data, anonymous volumes for node_modules
- ✅ .env file for secrets: docker compose reads it automatically
- ✅ Profiles for optional services (worker, monitoring, mail)
- ✅ compose.override.yml for local dev tweaks — add to .gitignore
- ❌ Never hardcode secrets in compose.yml — use environment variables or .env
- ❌ Never use compose for production — use Kubernetes or ECS instead
Docker Compose local stacks use the same images as production — the Docker best practices guide ensures those images are production-ready. External reference: Docker Compose documentation.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
