Docker Compose for Local Development
Define your whole stack in one file. The fastest way to onboard new engineers.
What Compose Solves
A modern app rarely runs alone. Your local development stack might include:
• Your app (Node, Python, Go, etc.)
• PostgreSQL
• Redis
• Localstack or minio for S3 emulation
• A message queue (RabbitMQ, Kafka)
• Maybe a search engine (Elasticsearch)
Setting up all this manually is painful. Engineers spend their first week installing services. Versions drift. "It works on my machine" reappears.
Docker Compose solves this. One YAML file describes the whole stack. One command (docker compose up) starts everything. Onboarding goes from days to minutes.
Compose is for development. For production, use Kubernetes or your cloud's container service. But for local dev, dev environments, simple integration tests, and small self-hosted stacks, Compose is unmatched.
A Simple Compose File
Save as compose.yml (or docker-compose.yml — both work):
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./src:/app/src # hot reload during dev
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432" # expose to host for psql access
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
Run:
docker compose up # foreground, see all logs
docker compose up -d # detached
docker compose down # stop and remove
docker compose down -v # also remove volumes (clean slate)
docker compose logs -f app # tail logs from one service
docker compose exec app sh # shell into running app
docker compose ps # see status
docker compose restart app # restart one service
Networking is automatic — app reaches db at hostname db. Compose creates a private network and adds a DNS entry for each service.
Common Patterns
Override files for different environments:
# compose.override.yml — local dev overrides (loaded automatically)
services:
app:
environment:
LOG_LEVEL: debug
volumes:
- ./src:/app/src
# compose.test.yml — testing overrides
services:
app:
environment:
NODE_ENV: test
Combine:
docker compose -f compose.yml -f compose.test.yml up
Profiles — services that aren't always needed:
services:
app: {...}
db: {...}
storybook:
image: my-storybook
profiles: [tools] # only starts when explicitly requested
mailhog:
image: mailhog/mailhog
profiles: [tools]
docker compose up # only app and db
docker compose --profile tools up # everything
Healthchecks ensure dependencies are READY, not just started:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
app:
depends_on:
db:
condition: service_healthy # waits for healthcheck to pass
Without service_healthy, your app starts as soon as Postgres's PROCESS starts — but Postgres might still be initializing.
Building from source:
app:
build:
context: .
dockerfile: Dockerfile.dev
args:
NODE_VERSION: '20'
Mix of build and pull — useful for monorepos:
services:
api:
build: ./api
web:
build: ./web
db:
image: postgres:16
Hot Reload for Fast Development
The best dev loop is editing code on your host and seeing the change reflected immediately in the running container.
Volume mount your source:
services:
app:
build: .
command: npm run dev # use a watch-mode command
volumes:
- ./src:/app/src
- /app/node_modules # exclude — don't overlay host's empty dir
environment:
CHOKIDAR_USEPOLLING: 'true' # Linux file events sometimes need polling
Common gotcha — that /app/node_modules line. The host bind mount of ./src:/app/src would normally hide the container's /app/node_modules if you also mounted .:/app. Listing the volume without a host path keeps the container's version.
For Python:
app:
command: uvicorn main:app --reload
volumes:
- ./:/app
For Go: use air (in the container) for live reload, or rebuild with each change.
Most modern frameworks have a watch/dev mode (Next.js, Vite, FastAPI, NestJS). Use it inside the container, mount your code, edit on your host, change appears immediately.
Beyond Local — When to Use Compose vs Not
Use Compose for:
• Local development environments
• Integration testing in CI
• Self-hosted personal/small projects (a Raspberry Pi running Compose works great)
• Quick demo environments
Don't use Compose for:
• Production at any meaningful scale
• Anything needing horizontal scaling, rolling deploys, or self-healing
For production, Kubernetes is the standard. Cloud-managed equivalents:
• ECS / Fargate (AWS) — simpler than K8s
• Cloud Run (GCP) — serverless containers
• App Runner, Lightsail, Railway, Fly.io — easier alternatives
The next two lessons cover Kubernetes — the technology that took the ideas of Docker and made them work at planet scale.
Investment note: every hour spent making your dev environment delightful pays back tenfold. A new engineer who can git clone && docker compose up and start contributing on day one is incredibly productive. Treat your Compose file as engineering infrastructure, not throwaway config.
⁂ Back to all modules