Docker Compose for Local Development Environments
"It works on my machine!" is the oldest excuse in software engineering. When a new developer joins your team,
they shouldn't spend three days configuring PostgreSQL, Redis, Node.js, and Python environments. With Docker
Compose, onboarding should take exactly one command: docker-compose up.
The Anatomy of a Development docker-compose.yml
A good development compose file differs from a production one. In production, code is baked into the image. In development, you want to use Volumes to mount your local source code into the container, enabling hot-reloading.
version: '3.8'
services:
api:
build:
context: ./backend
target: development # Multi-stage build targeting dev
ports:
- "8000:8000"
volumes:
- ./backend:/app # Hot-reloading magic
- /app/node_modules # Anonymous volume to prevent local node_modules from overwriting container's
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app_db
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app_db
ports:
- "5432:5432" # Expose so you can connect via DataGrip/DBeaver
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Key Best Practices
1. Anonymous Volumes for Dependencies
Notice the - /app/node_modules in the api service. When you bind-mount your local
directory to /app, your local (macOS/Windows) node_modules folder overrides the
container's (Linux) folder. This causes native C++ bindings (like bcrypt) to crash. The anonymous
volume tells Docker to use the container's node_modules instead.
2. Using depends_on with Healthchecks
depends_on only waits for the container to start, not for the process inside
it to be ready. If your API tries to connect to PostgreSQL while it's still initializing, it will crash. Use
healthchecks for robust startup ordering.
db:
image: postgres:15-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app_db"]
interval: 5s
timeout: 5s
retries: 5
api:
depends_on:
db:
condition: service_healthy
Overriding for Production
Never put production secrets or development volumes in your base docker-compose.yml. Use a base
file for common services, and create docker-compose.override.yml (which compose reads
automatically) for local development, and docker-compose.prod.yml for production deployments.
Conclusion
Docker Compose eliminates the friction of local environment setup. By encapsulating your app, database, cache, and message queues into a single declarative file, you guarantee parity across all developer machines and significantly reduce the "it works on my machine" syndrome.