Docker Compose for Production: A Practical Configuration Guide
Beyond docker-compose up
Docker Compose is not just a development tool. With the right configuration it handles production workloads cleanly — but the defaults are not production-ready. This guide covers the settings that matter.
---
Service Dependencies Done Right
depends_on alone does not wait for a service to be ready — only for it to have started. Use healthchecks to wait for readiness:
services:
postgres:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devops -d myapp"]
interval: 10s
timeout: 5s
retries: 5
backend:
image: myapp-backend
depends_on:
postgres:
condition: service_healthy # waits for healthcheck to pass
---
Environment Variables
services:
backend:
env_file:
- .env # load from file (gitignored)
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:${POSTGRES_PASSWORD}@postgres:5432/myapp
# ${VAR} is substituted from .env or shell environment at compose-up time
Never commit secrets to docker-compose.yml. Keep them in .env and load via env_file.
---
Resource Limits
services:
backend:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
Without limits, a misbehaving container can consume all host memory and OOM-kill other services.
---
Logging
services:
backend:
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5" # 100 MB max total per service
Default logging has no size limit — logs grow unbounded and fill the disk.
---
Restart Policy
services:
backend:
restart: unless-stopped # restart on crash, stop on explicit docker stop
Use unless-stopped in production. always restarts even after a deliberate docker stop, which makes maintenance harder.
---
Multi-Environment Override Files
# Base config shared by all environments
docker-compose.yml
# Production overrides
docker-compose.prod.yml
# Run with both files merged
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Override file only needs to contain the differences:
# docker-compose.prod.yml
services:
frontend:
build:
target: prod # use the prod build stage
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
---
Volumes for Persistence
services:
postgres:
volumes:
- postgres_data:/var/lib/postgresql/data # named volume (managed by Docker)
- ./backup:/backup:ro # bind mount (host path)
volumes:
postgres_data: # declare named volume
Named volumes survive docker compose down. Use docker compose down -v to also remove them (wipes data).
---
Networking
services:
backend:
networks:
- app_net
- infra_net
frontend:
networks:
- app_net # can reach backend, cannot reach infra_net
networks:
app_net: {}
infra_net:
external: true # created outside this compose file
Put each service only on the networks it needs. Avoid putting everything on the same network.
Practice Docker Compose in the ShellGenius Docker Labs — the compose-stack and network-microservices challenges use real multi-service scenarios.