Docker Compose for Production: Tips and Best Practices
Docker Compose is best known as a development tool — docker compose up starts your entire stack with one command. But Compose is also capable of running in production, especially for small to medium deployments where a full Kubernetes cluster is overkill.
This guide covers production-hardening techniques for Docker Compose: multi-stage builds, health checks, logging drivers, secret management, resource constraints, and orchestration patterns with Docker Swarm.
Multi-Stage Builds
Multi-stage builds produce smaller, more secure images by separating build environment from runtime:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM nginx:stable-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]The final image contains only the nginx runtime and static files — no Node.js, no source code, no dev dependencies. This reduces the attack surface and image size from ~500 MB to ~25 MB.
Multi-Stage with Build Arguments
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine AS builder
ARG APP_ENV=production
RUN echo "Building for $APP_ENV"
FROM node:${NODE_VERSION}-alpine AS runtime
COPY --from=builder /app/dist /app
CMD ["node", "/app/server.js"]Override build args in compose.yaml:
services:
app:
build:
context: .
args:
NODE_VERSION: 22
APP_ENV: stagingHealth Checks
Health checks tell Docker when a container is truly ready. Unlike the process-based check (is the process running?), a health check tests application-level readiness:
services:
api:
build: .
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40sDependent Service Startup
Health checks enable ordered startup with depends_on:
services:
database:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
build: .
depends_on:
database:
condition: service_healthyWithout the health check condition, depends_on only waits for the container process to start — which happens before the database is ready to accept connections. The service_healthy condition waits until the health check passes.
Logging Configuration
Structured Logging
Configure your application to emit JSON logs, then use Docker’s logging drivers:
services:
app:
build: .
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"Centralized Logging with Loki
For production, forward logs to a centralized system:
services:
app:
build: .
logging:
driver: "loki"
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-retries: "2"
loki-max-backoff: "100ms"
loki:
image: grafana/loki:3.0
ports:
- "3100:3100"
promtail:
image: grafana/promtail:3.0
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yml:/etc/promtail/config.ymlThe json-file driver with rotation prevents disks from filling. For structured querying, aggregate logs with Loki + Grafana or the ELK stack.
Secrets Management
Never hardcode secrets in compose.yaml or Dockerfiles. Docker Compose supports secrets natively:
services:
app:
build: .
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
environment: "API_KEY"Inside the container, secrets appear as files at /run/secrets/<name>:
# app.py
with open("/run/secrets/db_password") as f:
db_password = f.read().strip()For Docker Swarm mode, secrets are encrypted during transit and at rest. Always add secrets/ to .gitignore.
Resource Constraints
Limit CPU and memory per service to prevent one container from starving others:
services:
app:
build: .
deploy:
resources:
limits:
cpus: "0.5"
memory: "512M"
reservations:
cpus: "0.25"
memory: "256M"
worker:
build: .
deploy:
resources:
limits:
memory: "1G"When a container exceeds its memory limit, Docker kills it with an OOM error. CPU limits throttle the container at the kernel level. Set reservations for guaranteed minimum resources and limits for hard caps.
OOM Priority
services:
database:
image: postgres:16
deploy:
resources:
limits:
memory: "2G"
oom_kill_disable: true
oom_score_adj: -500oom_kill_disable: true prevents the database from being killed under memory pressure. oom_score_adj adjusts the OOM killer priority — lower values are less likely to be killed. Use this carefully for critical infrastructure services only.
Networking
Custom Networks
services:
api:
build: .
networks:
- frontend
- backend
database:
image: postgres:16
networks:
- backend
expose:
- "5432"
nginx:
image: nginx:stable-alpine
networks:
- frontend
ports:
- "80:80"
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external accessThe internal: true network blocks external access — the database is only reachable by services on the same network. This is defense in depth: even if the API is compromised, the database port is not directly exposed.
DNS and Service Discovery
Compose provides built-in DNS resolution. Services reach each other by service name:
services:
api:
build: .
environment:
DATABASE_URL: "postgres://user:password@database:5432/db"No need to hardcode IP addresses. Compose automatically registers service names in an internal DNS.
Orchestration with Docker Swarm
For multi-node production deployments, Docker Swarm provides native orchestration:
version: "3.8"
services:
app:
image: myapp:latest
deploy:
mode: replicated
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
restart_policy:
condition: on-failure
max_attempts: 3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]Deploy with:
docker stack deploy -c compose.yaml myappSwarm handles rolling updates, load balancing, and node failure. The start-first update order starts a new container before stopping the old one, ensuring zero-downtime deployments.
Configs for Shared Configuration
services:
app:
image: myapp:latest
configs:
- source: app_config
target: /etc/app/config.yml
configs:
app_config:
file: ./config.ymlUnlike secrets, configs are stored unencrypted and are useful for non-sensitive configuration files like nginx configs or application settings.
Backup and Recovery
Database Backups
services:
db-backup:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
entrypoint: |
sh -c '
while true; do
pg_dump -h database -U postgres mydb > /backups/db-$(date +%Y%m%d).sql
sleep 86400
done'
depends_on:
database:
condition: service_healthyRun backups as a separate service, not a cron job inside the database container. This keeps the database image focused and allows backup tools to be updated independently.
Best Practices
- Pin image tags to specific versions — Never use
:latestin production. Usepostgres:16.4or specific SHAs. - Use read-only root filesystems —
read_only: trueprevents writes to the container filesystem. Mount tmpfs for ephemeral writes. - Set user directives — Avoid running as root:
user: "1000:1000"with matching UID/GID. - Enable restart policies —
restart: unless-stoppedfor daemon services. - Scan images for vulnerabilities — Use
docker scoutor Trivy in your CI pipeline. - Use
.dockerignore— Excludenode_modules,.git, and build artifacts to keep build context small.
Summary
Docker Compose is production-capable when configured correctly. Multi-stage builds produce minimal images. Health checks ensure reliable service startup order. Logging drivers and centralized log aggregation keep operations manageable. Secrets and network isolation reduce the attack surface. Resource constraints prevent noisy neighbors. For multi-node deployments, Docker Swarm adds orchestration without leaving the Compose ecosystem.
Production Docker Compose Patterns
Multi-Environment Configuration
Docker Compose supports multiple configuration files for different environments. The base docker-compose.yml defines shared service definitions, while environment-specific files override settings. A production compose file (docker-compose.prod.yml) might set resource limits, enable health checks, configure log rotation, and specify production environment variables. The override mechanism uses docker compose -f docker-compose.yml -f docker-compose.prod.yml up. YAML anchors and extensions reduce duplication across services — a logging configuration anchor can be referenced by all services using <<: *logging-config. Environment variables are sourced from .env files, with separate files for each environment and secrets managed through Docker secrets or external vaults.
Health Checks and Self-Healing
Docker Compose supports health checks that monitor container health using commands like curl, wget, or application-specific endpoints. The healthcheck directive specifies the test command, interval, timeout, retries, and start period. When a service depends on another, condition: service_healthy ensures the dependency is fully ready before starting dependent services. Docker Compose’s restart policies — always, unless-stopped, on-failure — provide automatic recovery from crashes. For more sophisticated orchestration, consider Docker Swarm (built into Docker) or multi-node configurations. In production, health checks combined with reverse proxy load balancers (Traefik, Nginx) enable zero-downtime deployments through rolling updates with controlled service scaling.
Networking and Security
Docker Compose creates a default bridge network for all services in the compose file, enabling DNS-based service discovery. Production deployments should define explicit networks and isolate sensitive services. An internal network for backend services (database, cache) should have no external access, while a public-facing network connects the reverse proxy to the outside. This network segmentation limits the blast radius if a service is compromised. Secrets management in production requires avoiding environment variables for sensitive data — use Docker secrets (available in Swarm mode) or mount encrypted volumes. The security_opt and cap_drop directives reduce container privileges: cap_drop: ALL followed by cap_add for only required capabilities implements the principle of least privilege for each container.
Frequently Asked Questions
Is Docker Compose production-ready? Docker Compose is suitable for small-scale production deployments with simple orchestration needs — single server or small cluster, low traffic volume, and acceptable downtime during maintenance. For high-availability, auto-scaling, or zero-downtime deployments, Kubernetes or Docker Swarm provides the necessary orchestration capabilities.
How do I handle secrets in Docker Compose? For Docker Compose in Swarm mode, use Docker secrets which are encrypted at rest and in transit. For standalone Compose, use environment variables from secure sources, external secret management tools (HashiCorp Vault, AWS Secrets Manager), or encrypted .env files. Never commit secrets to version control.
How do I scale services with Docker Compose? Docker Compose supports deploy.replicas in version 3+ files when used with Docker Swarm. For single-node deployments, use multiple compose service definitions with different ports or container names. For true horizontal scaling across machines, migrate to Docker Swarm or Kubernetes.
For a comprehensive overview, read our article on Blue Green Deployments.
For a comprehensive overview, read our article on Ci Cd Pipeline Guide.