Docker Compose Patterns for Production
Production-ready Docker Compose patterns: override files, healthchecks, resource limits, logging, and secrets management.
What you'll learn
- ✓How to structure Compose files for dev, staging, and production
- ✓Resource limits, logging, and restart policies for production
- ✓Secrets management and environment variable patterns
Prerequisites
- •Basic Docker Compose knowledge — see Docker Compose Basics
- •Familiarity with Docker networking
Docker Compose is often seen as a development tool, but with the right patterns it works well for single-host production deployments, staging environments, and CI pipelines. This article covers the patterns that separate a toy docker-compose.yml from a production-grade one.
Override Files for Environment Separation
The foundation of production Compose is the override file pattern. Keep a base file with shared configuration and layer environment-specific settings on top.
docker-compose.yml — the base:
services:
api:
build:
context: .
target: production
environment:
- NODE_ENV=production
networks:
- backend
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
networks:
backend:
docker-compose.override.yml — development overrides (loaded automatically):
services:
api:
build:
target: development
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DEBUG=api:*
db:
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=devpassword
docker-compose.prod.yml — production overrides:
services:
api:
image: registry.example.com/myapp:${APP_VERSION}
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 256M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
restart: unless-stopped
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Run with explicit file selection:
# Development (uses override automatically)
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Resource Limits
Always set resource limits in production. Without them, a single container can consume all host memory and bring down everything else.
services:
api:
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
The limits cap what the container can use. The reservations guarantee minimum resources. If a container exceeds its memory limit, Docker kills it with an OOM error.
For Java applications, align JVM heap settings with container limits:
services:
java-api:
image: myapp:latest
environment:
- JAVA_OPTS=-Xmx768m -Xms256m
deploy:
resources:
limits:
memory: 1G
Healthchecks and Dependency Ordering
Production containers must have healthchecks. Without them, Docker considers a container healthy the moment it starts, even if the application inside is still initializing.
services:
api:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
The start_period gives the container time to initialize before Docker starts counting failed checks. The depends_on with condition: service_healthy ensures services start in the right order.
Logging Configuration
Docker’s default logging driver stores logs as JSON files with no size limit. In production, logs will fill your disk.
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
tag: "{{.Name}}/{{.ID}}"
For centralized logging, forward to a log aggregator:
services:
api:
logging:
driver: syslog
options:
syslog-address: "tcp://logserver:514"
tag: "api"
Or run a sidecar log collector:
services:
api:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
log-forwarder:
image: fluent/fluent-bit:latest
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
depends_on:
- api
Secrets Management
Never put passwords in environment variables in your Compose file. Use Docker secrets or external files.
File-based secrets:
services:
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Environment files for non-sensitive configuration:
services:
api:
env_file:
- .env.common
- .env.production
Keep .env files out of version control. Add them to .gitignore and .dockerignore.
Restart Policies
Production containers should restart automatically after failures:
services:
api:
restart: unless-stopped
The options are:
no— never restart (default)on-failure— restart only on non-zero exit codesalways— restart no matter whatunless-stopped— restart unless explicitly stopped by the user
Use unless-stopped for most production services. Use on-failure for one-shot tasks that should retry on failure but not loop forever.
Networking Patterns
Isolate services into separate networks. Only expose what needs to be exposed:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend
redis:
networks:
- backend
networks:
frontend:
backend:
internal: true
The internal: true flag on the backend network prevents containers on that network from reaching the internet. The database and Redis are only accessible to the API, never directly from the outside.
Named Volumes with Backup Patterns
Use named volumes for persistent data and back them up:
volumes:
pgdata:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres
Add a backup service:
services:
db-backup:
image: postgres:16-alpine
volumes:
- ./backups:/backups
environment:
- PGHOST=db
- PGUSER=app
- PGPASSWORD_FILE=/run/secrets/db_password
entrypoint: >
sh -c 'pg_dump -Fc app > /backups/backup_$$(date +%Y%m%d_%H%M%S).dump'
depends_on:
db:
condition: service_healthy
profiles:
- backup
secrets:
- db_password
Run backups on demand with:
docker compose --profile backup run --rm db-backup
Production Checklist
Before deploying with Compose in production, verify:
- All services have
restart: unless-stopped - All services have healthchecks
- Resource limits are set for CPU and memory
- Logging has size limits configured
- Secrets use files or external secret stores, not inline environment variables
- Internal services are on isolated networks
- Volumes use named volumes, not bind mounts for data
- Images are pinned to specific tags or digests, not
latest - A
.envfile is excluded from version control
Summary
Docker Compose works for production when you apply the right patterns: override files for environment separation, resource limits to prevent runaway containers, healthchecks for proper dependency ordering, structured logging, and secrets management. These patterns give you a reproducible, maintainable deployment that is easy for any team member to understand and operate.
Related articles
- Docker Docker Compose Watch: Hot Reload for Containers
Learn how to use Docker Compose Watch to automatically sync file changes and rebuild containers during development. Faster feedback loops without manual restarts.
- Docker Docker BuildKit Advanced Features You Should Be Using
Master BuildKit's cache mounts, secret mounts, SSH forwarding, and parallel builds to speed up Docker image creation and keep secrets out of layers.
- Docker Docker Layer Caching Strategies to Speed Up Builds
Master Docker layer caching: instruction ordering, cache mounts, BuildKit inline cache, and CI registry caching for faster builds.
- Docker Docker Multi-Stage Builds: Advanced Optimization Techniques
Go beyond basic multi-stage builds. Learn cache mounts, parallel stages, and distroless bases to shrink Docker images by 90%.