Introduction
Docker Compose is excellent for local development, but running it in production requires additional considerations: high availability, log management, secrets handling, health checks, and restart policies. This guide teaches you to configure Docker Compose for production workloads and understand when to move to Kubernetes or Docker Swarm instead.
Production vs Development Compose Files
Use multiple compose files with override pattern:
# docker-compose.yml (base - shared between dev and prod)
version: '3.8'
services:
app:
image: company/myapp:${APP_VERSION:-latest}
environment:
- DATABASE_URL
- REDIS_URL
networks:
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:15
environment:
- POSTGRES_DB
- POSTGRES_USER
- POSTGRES_PASSWORD
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
- backend
volumes:
postgres-data:
networks:
backend:
driver: bridge# docker-compose.prod.yml (production overrides)
version: '3.8'
services:
app:
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.company.com`)"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
postgres:
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf
command: postgres -c config_file=/etc/postgresql/postgresql.conf
deploy:
resources:
limits:
cpus: '4.0'
memory: 4G
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./ssl:/etc/ssl/certs
- /var/log/nginx:/var/log/nginx
depends_on:
- app
volumes:
postgres-data:
driver: local
driver_opts:
type: none
device: /data/postgres # Mount to fast SSD
o: bind# Run in production (merges both files)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dSecrets Management
Never put secrets in docker-compose.yml directly:
# Create .env file (never commit to git!)
cat > /opt/app/.env << 'EOF'
APP_VERSION=v3.5.2
DATABASE_URL=postgresql://appuser:Str0ngP@ss!@postgres:5432/appdb
POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=Str0ngP@ss!
REDIS_URL=redis://:RedisP@ss!@redis:6379
REDIS_PASSWORD=RedisP@ss!
EOF
chmod 600 /opt/app/.env
# Run with env file
docker compose --env-file /opt/app/.env up -dUsing Docker secrets (Swarm mode):
# Create secrets
echo "Str0ngP@ss!" | docker secret create postgres_password -
echo "RedisP@ss!" | docker secret create redis_password -
# Reference in compose (Swarm only)
version: '3.8'
services:
postgres:
secrets:
- postgres_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
postgres_password:
external: trueHealth Checks and Dependencies
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s # Grace period during startup
# App only starts after DB is healthy
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Entrypoint waits for dependencies
entrypoint: >
sh -c "
echo 'Waiting for postgres...'
until pg_isready -h postgres -U app; do sleep 1; done
echo 'Running migrations...'
python manage.py migrate
echo 'Starting server...'
gunicorn app:app -w 4 -b 0.0.0.0:8080
"Logging Strategy
# Centralized logging with Loki
services:
app:
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-labels: "job=app,environment=production"
loki-retries: "5"
loki-batch-size: "400"
max-size: "50m" # Local backup in case Loki is down
# OR: Use json-file and ship with Promtail sidecar
app:
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
tag: "{{.Name}}/{{.ID}}"
promtail:
image: grafana/promtail:latest
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yaml:/etc/promtail/config.yaml
command: -config.file=/etc/promtail/config.yamlDeployment Script
#!/bin/bash
# deploy.sh - Zero-downtime production deployment
set -euo pipefail
APP_DIR="/opt/myapp"
COMPOSE_CMD="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
echo "=== Deploying $(APP_VERSION) to production ==="
cd "$APP_DIR"
# Pull new images first
$COMPOSE_CMD pull app
# Rolling restart (if using multiple replicas via nginx)
# Scale up with new version, then scale down old
$COMPOSE_CMD up -d --no-deps --scale app=4 app
sleep 15 # Wait for new instances to be healthy
# Verify health
if ! curl -sf http://localhost/health > /dev/null; then
echo "Health check failed! Rolling back..."
$COMPOSE_CMD up -d --no-deps --scale app=2 app
exit 1
fi
# Scale back to normal
$COMPOSE_CMD up -d --no-deps --scale app=2 app
echo "=== Deployment complete ==="Monitoring Container Resource Usage
# Live stats for all containers
docker stats
# Stats in specific format for monitoring
docker stats --format "table {{.Name}} {{.CPUPerc}} {{.MemUsage}} {{.NetIO}} {{.BlockIO}}"
# Check logs with timestamps
docker compose logs -f --timestamps app
# Container events
docker events --filter 'type=container'Backup Strategy for Compose Services
#!/bin/bash
# backup.sh - Backup production database
BACKUP_DIR="/backup/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# Dump database from running container
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "$BACKUP_DIR/backup_${DATE}.sql.gz"
# Keep only last 30 days
find "$BACKUP_DIR" -name "backup_*.sql.gz" -mtime +30 -delete
# Upload to S3
aws s3 cp "$BACKUP_DIR/backup_${DATE}.sql.gz" "s3://company-backups/postgres/${DATE}.sql.gz"Docker Compose in production works well for small to medium deployments (single server or few servers). When you need multi-host scheduling, auto-healing, or fine-grained resource management across a large cluster, graduate to Kubernetes. But for many teams, a well-configured Compose setup is simpler to operate and debug.
