Docker Compose in Production: Best Practices for Multi-Service Deployments
Docker Compose simplifies multi-service deployments, but production use requires careful configuration. Learn security, performance, and reliability best practices.
Docker Compose is fantastic for development: define your services in a YAML file, run docker compose up, and everything starts. But using Compose in production requires more discipline. You need health checks, resource limits, proper networking, secret management, and restart policies.
This guide covers everything you need to run Docker Compose reliably in production environments.
Basic vs Production Compose Files
Development Compose (Simple)
version: '3'
services:
web:
build: .
ports:
- "80:80"
db:
image: postgres
environment:
POSTGRES_PASSWORD: password123
This works for local development but is unsafe for production.
Production Compose (Hardened)
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile.prod
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
env_file:
- .env.production
read_only: true
tmpfs:
- /tmp:size=100M
user: "1000:1000"
db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
volumes:
pgdata:
driver: local
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password.txt
Let's break down each production improvement.
Health Checks: Detect Failures Automatically
Health checks let Docker determine if a service is actually working, not just running.
Basic Health Check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Fields:
test: Command to run (must exit 0 for healthy, 1 for unhealthy)interval: How often to check (default: 30s)timeout: Max time for check to complete (default: 30s)retries: Failed checks before marking unhealthy (default: 3)start_period: Grace period for container startup (default: 0s)
Common Health Check Patterns
HTTP endpoint:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
Database connection:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
Custom script:
healthcheck:
test: ["CMD", "/app/healthcheck.sh"]
Process check:
healthcheck:
test: ["CMD-SHELL", "pidof nginx || exit 1"]
Using Health Checks
Depend on healthy services:
services:
api:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
Monitor health status:
docker compose ps
# Shows health status for each service
docker inspect --format='{{.State.Health.Status}}' <container>
Resource Limits: Prevent Resource Exhaustion
Without limits, a single container can consume all host resources.
CPU and Memory Limits
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
Limits: Maximum resources the container can use
Reservations: Minimum resources guaranteed to the container
Find the Right Limits
Monitor actual usage:
docker statsSet limits slightly above peak usage:
- If app uses 400MB RAM normally, set limit to 512MB
- Add headroom for traffic spikes
Test under load:
# Simulate high traffic ab -n 10000 -c 100 http://localhost/ # Monitor resource usage docker stats
OOM Kill Prevention
If a container exceeds its memory limit, it's killed (OOM killed). Prevent this:
deploy:
resources:
limits:
memory: 512M
Monitor for OOM kills:
dmesg | grep -i "out of memory"
docker inspect <container> | grep OOMKilled
Restart Policies: Automatic Recovery
When containers crash, restart them automatically.
Available Policies
restart: no # Never restart (default)
restart: on-failure # Restart only if exit code != 0
restart: unless-stopped # Always restart unless manually stopped
restart: always # Always restart, even after daemon restart
Recommendation
restart: unless-stopped
This restarts containers after crashes and host reboots, but respects manual stops.
Exponential Backoff
Docker uses exponential backoff for restart delays:
- First restart: immediate
- Second restart: 100ms delay
- Third restart: 200ms delay
- Fourth restart: 400ms delay
- Maximum delay: 1 minute
Secret Management: Secure Credentials
Never hardcode passwords in compose files.
Docker Secrets
Create a secret file:
mkdir -p secrets
echo "supersecretpassword" > secrets/db_password.txt
chmod 600 secrets/db_password.txt
Reference in compose:
services:
db:
image: postgres
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
The secret is mounted at /run/secrets/db_password inside the container.
Environment Files
For non-sensitive config, use .env files:
services:
web:
env_file:
- .env.production
# .env.production
NODE_ENV=production
LOG_LEVEL=info
API_URL=https://api.example.com
Add to .gitignore:
# .gitignore
.env.production
secrets/
External Secret Managers
For enterprise deployments, integrate with:
- HashiCorp Vault: Dynamic secrets, audit logging
- AWS Secrets Manager: Cloud-native secret storage
- Azure Key Vault: Microsoft cloud secrets
- Google Secret Manager: GCP secret storage
Networking: Isolation and Security
Separate services into different networks for security.
Frontend and Backend Networks
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
Assign services:
services:
web:
networks:
- frontend # Accessible from outside
- backend # Can reach backend services
api:
networks:
- backend # Only accessible via web
db:
networks:
- backend # Only accessible via api
Traffic flow:
Internet → web (frontend+backend) → api (backend) → db (backend)
The internal: true flag prevents backend network from accessing the internet.
Port Binding
Bind to localhost unless external access is needed:
ports:
- "127.0.0.1:8080:80" # Localhost only
For public services, use a reverse proxy (Nginx, Traefik) instead of direct port exposure.
Volume Management: Persistent Data
Use named volumes for data that must persist.
Named Volumes
volumes:
pgdata:
driver: local
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
Named volumes are managed by Docker, stored in /var/lib/docker/volumes/.
Bind Mounts (Development Only)
volumes:
- ./config:/etc/app/config:ro # Read-only mount
Avoid bind mounts in production (path dependencies, permission issues).
Volume Backups
Backup named volumes:
docker run --rm \
-v pgdata:/source:ro \
-v $(pwd):/backup \
alpine tar czf /backup/pgdata-backup.tar.gz -C /source .
Restore:
docker run --rm \
-v pgdata:/dest \
-v $(pwd):/backup \
alpine tar xzf /backup/pgdata-backup.tar.gz -C /dest
Logging: Capture and Rotate Logs
Prevent logs from filling up disk space.
Log Driver Configuration
services:
web:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
max-size: Maximum size per log file (10 MB)max-file: Maximum number of log files to keep (3)
Total disk usage: 30 MB per service.
View Logs
# All services
docker compose logs
# Specific service
docker compose logs web
# Follow logs
docker compose logs -f web
# Last 100 lines
docker compose logs --tail=100 web
Centralized Logging
Forward logs to external systems:
logging:
driver: syslog
options:
syslog-address: "tcp://logs.company.com:514"
tag: "myapp-{{.Name}}"
Deployment Workflow
Step 1: Validate Compose File
docker compose config
# Shows resolved configuration
Step 2: Build Images
docker compose build --no-cache
Step 3: Test Locally
docker compose up -d
docker compose ps
docker compose logs
Step 4: Deploy to Server
Upload compose file and secrets:
scp docker-compose.prod.yml user@server:/opt/myapp/
scp -r secrets/ user@server:/opt/myapp/
On server:
cd /opt/myapp
docker compose -f docker-compose.prod.yml up -d
Step 5: Verify Deployment
# Check all services running
docker compose ps
# Check health
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
# Test endpoints
curl http://localhost:8080/health
Step 6: Update Strategy
Zero-downtime updates:
# Pull new images
docker compose pull
# Recreate services one at a time
docker compose up -d --no-deps --build web
docker compose up -d --no-deps --build api
# Remove old images
docker image prune -f
FoxTerm Compose Integration
FoxTerm simplifies Compose deployments.
Visual Stack Management
View all Compose stacks:
- Service status and health
- Resource usage per container
- Port mappings
- Volume mounts
One-Click Deploy
- Paste or upload
docker-compose.yml - Configure environment variables via UI
- Click "Deploy"
- FoxTerm uploads files, runs
docker compose up -d, and shows real-time logs
Monitoring Dashboard
Monitor all services:
- CPU/memory usage graphs
- Log viewer with filtering
- Health check status
- Restart history
Stack Templates
Save common stacks as templates:
- LAMP stack (Linux, Apache, MySQL, PHP)
- MEAN stack (MongoDB, Express, Angular, Node)
- Monitoring stack (Prometheus, Grafana, Alertmanager)
Deploy with one click.
Conclusion
Docker Compose in production requires attention to health checks, resource limits, secrets, networking, and logging. Apply these best practices, and you'll have reliable, secure multi-service deployments.
FoxTerm makes managing Compose stacks visual and straightforward, whether you're running one service or twenty.
FoxTerm Pro includes Docker Compose management. Download at foxterm.app.