Docker Compose in Production ā Patterns and Limitations
Use Docker Compose in production for small deployments: resource limits, restart policies, environment variable injection, secrets management, and when to move beyond Docker Compose to Kubernetes.
Docker Compose in Production ā Honest Assessment
Docker Compose is appropriate for production when:
ā Small deployment (1-5 services)
ā Single server or VM (no cross-host orchestration needed)
ā Team does not have Kubernetes operational experience
ā Workload does not require per-service auto-scaling
Docker Compose is NOT appropriate for production when:
ā You need auto-scaling (Compose has no horizontal scaling built-in)
ā You need rolling updates with zero downtime (Compose restarts services)
ā You have more than one host node
ā High availability is a requirement (single point of failure)
For most clinical SaaS platforms with under 500 concurrent users:
Docker Compose on a VM (or Azure App Service with containers) is sufficient.
You can always migrate to AKS later if you outgrow it.Production Compose File
# docker-compose.prod.yml
services:
prescription-service:
image: clinical/prescription-service:${IMAGE_TAG} # from CI/CD pipeline
restart: unless-stopped
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ConnectionStrings__Clinical=${DB_CONNECTION_STRING}
- APPLICATIONINSIGHTS_CONNECTION_STRING=${AI_CONNECTION_STRING}
deploy:
resources:
limits:
cpus: '1.0' # max 1 CPU core
memory: 512M # max 512 MB RAM
reservations:
cpus: '0.25'
memory: 128M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
networks: [backend]
nginx:
image: nginx:1.25-alpine
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
prescription-service:
condition: service_healthy
networks: [frontend, backend]
networks:
frontend:
backend:
internal: trueEnvironment Variable Injection
# .env file (NOT committed to source control ā add to .gitignore)
IMAGE_TAG=v1.2.3
DB_CONNECTION_STRING=Server=clinical-sql;Database=Clinical;User Id=app;Password=...
AI_CONNECTION_STRING=InstrumentationKey=...;IngestionEndpoint=...
DB_PASSWORD=YourStrongPassword123!
REDIS_PASSWORD=AnotherStrongPassword456!# docker-compose.prod.yml reads from .env automatically:
services:
prescription-service:
image: clinical/prescription-service:${IMAGE_TAG}
environment:
- DB_CONNECTION_STRING=${DB_CONNECTION_STRING}# Deploy command:
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.prod up -d
# Verify running:
docker compose ps
docker compose logs prescription-service --tail=50 -fSecrets Management in Compose
# Docker secrets (Docker Swarm) ā not available in standalone Compose
# For standalone Compose, use:
# Option 1: Environment variables from .env file (see above)
# Most common for small deployments
# Option 2: Docker secrets files (without Swarm)
secrets:
db_password:
file: ./secrets/db_password.txt # file on the host ā secured by filesystem permissions
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
secrets:
- db_password
environment:
- SA_PASSWORD_FILE=/run/secrets/db_password
# Some images support _FILE suffix to read from /run/secrets/
# Option 3: External secret store (Key Vault) ā best for production
# App reads secrets from Azure Key Vault via Managed Identity at runtime
# Compose provides only the Key Vault URI:
services:
prescription-service:
environment:
- KeyVaultUri=https://clinical-kv.vault.azure.net/
# App calls Key Vault at startup ā no secrets in compose fileRestart Policies
restart: "no" # Never restart (default for development)
restart: always # Always restart ā even on explicit stop
restart: unless-stopped # Restart unless explicitly stopped by operator (recommended for production)
restart: on-failure # Restart only on non-zero exit code
# For production services:
restart: unless-stopped
# ā Restarts automatically on crash
# ā Does NOT restart if you manually ran: docker compose stop prescription-service
# ā Survives host VM reboots (if Docker daemon is configured to start on boot)
# Ensure Docker daemon starts on boot (Linux):
# systemctl enable dockerRolling Deployment Pattern
# Compose doesn't have native rolling updates ā implement with a script
#!/bin/bash
# deploy.sh
NEW_TAG=$1 # e.g., ./deploy.sh v1.2.4
# Pull new image
docker compose pull prescription-service
# Update IMAGE_TAG and restart service
IMAGE_TAG=$NEW_TAG docker compose up -d --no-deps prescription-service
# Wait for health check to pass (max 60 seconds)
for i in $(seq 1 12); do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' clinical_prescription-service_1)
if [ "$STATUS" = "healthy" ]; then
echo "Deployment successful ā service is healthy"
exit 0
fi
echo "Waiting for health check... ($STATUS)"
sleep 5
done
echo "Deployment failed ā service did not become healthy"
docker compose logs prescription-service --tail=50
exit 1Production issue I've seen: A team deployed a clinical system using Docker Compose on a VM with no resource limits. A prescription report endpoint had an N+1 query that loaded thousands of rows. A ward manager ran a report for 6 months of data, the service consumed all available memory, the Linux OOM killer terminated the process, and the entire VM became unresponsive for 10 minutes (SQL Server, Redis, and the API all died together). Adding resource limits (
memory: 512M) and the--oom-kill-disable=falseflag would have let Docker kill only the offending service, not the entire host. Resource limits are not optional in production ā they are the difference between a service failure and a full system outage.
Key Takeaway
Docker Compose is production-appropriate for small single-server deployments ā not for multi-node or auto-scaling workloads. Always set resource limits (
deploy.resources.limits) to prevent one service from consuming the whole host. Userestart: unless-stoppedfor production services. Never commit secrets ā use.envfiles excluded from source control, or external secret stores (Key Vault). Implement health checks on every service and make nginx depend on them. For rolling deployments, script the pull-restart-wait-health pattern since Compose has no native rolling update.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.