πΌ Docker Compose for Production
Compose isn't just a development toy. For small-to-medium deployments β where Kubernetes would be overkill β a well-structured Compose setup gives you multi-container orchestration with health checks, secrets, networks, and scaling, all in one readable file.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Contrast a development Compose file with a production one
- Structure config with base + override files and profiles
- Configure health checks, resource limits, restart policies, and secrets
- Apply network segmentation to isolate backend services
- Scale and update services with Docker Swarm, and know when to move to Kubernetes
Estimated Time: 50β65 minutes β’ Difficulty: Intermediate
Hands-on: Turn a development Compose file into a secure, resource-limited production stack with a base/override structure.
In This Lesson
Compose in Production
Docker Compose describes a multi-container application in one declarative YAML file. In development you lean on bind-mounted source, hot reload, and plaintext passwords. In production those same conveniences become liabilities β you swap them for pinned images, health checks, resource limits, secrets, and segmented networks.
π‘ A useful analogy: Compose is the conductor of an orchestra, coordinating many containers to play together. In rehearsal (dev) you allow improvisation; in the live concert (prod) every entrance is precisely cued and rehearsed for reliability.
π‘ A note on the version: key
Modern Compose (the docker compose v2 plugin) ignores the old top-level version: field β it's obsolete and can be removed. The examples below omit it; if you see it in older files, it's harmless but no longer required.
Dev β Prod Configuration
Here's a typical development Compose file β optimized for convenience:
# compose.yaml (development)
services:
web:
build: ./frontend
ports: ["3000:3000"]
volumes:
- ./frontend:/app # bind mount for hot reload
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev
api:
build: ./backend
ports: ["4000:4000"]
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:password@db:5432/devdb
depends_on: [db]
db:
image: postgres:16
ports: ["5432:5432"] # exposed for local tools
environment:
- POSTGRES_PASSWORD=password # plaintext β fine for dev only
- POSTGRES_DB=devdb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
The production version swaps convenience for reliability and security: pinned images, resource limits, health checks, secrets, and a segmented backend network the outside world cannot reach.
# compose.prod.yaml (production)
services:
web:
image: ${REGISTRY}/frontend:${TAG}
deploy:
replicas: 2
resources:
limits: { cpus: '0.5', memory: 512M }
restart_policy: { condition: any, max_attempts: 3 }
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks: [frontend]
ports: ["80:3000"]
environment:
- NODE_ENV=production
- API_URL=http://api:4000
api:
image: ${REGISTRY}/backend:${TAG}
deploy:
resources:
limits: { cpus: '1.0', memory: 1G }
restart_policy: { condition: any }
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:4000/health"]
interval: 30s
timeout: 5s
retries: 3
networks: [frontend, backend]
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://appuser@db:5432/proddb
depends_on:
db: { condition: service_healthy }
secrets: [db_password]
db:
image: postgres:16-alpine
deploy:
resources:
limits: { cpus: '2.0', memory: 2G }
restart_policy: { condition: any }
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db_data:/var/lib/postgresql/data
networks: [backend]
environment:
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- POSTGRES_DB=proddb
secrets: [db_password]
networks:
frontend:
backend:
internal: true # no route to the outside world
volumes:
db_data:
secrets:
db_password:
file: ./secrets/db_password.txt
Override Files & Profiles
Base + override files
Keep one base file with what's common to every environment, then layer environment-specific overrides on top. Compose merges them left-to-right.
# Files
compose.yaml # base: images, networks, dependencies
compose.override.yaml # auto-applied in dev (bind mounts, ports)
compose.prod.yaml # production: limits, health checks, secrets
# Deploy to production by combining base + prod override
docker compose -f compose.yaml -f compose.prod.yaml up -d
β Why this beats one giant file
Common structure lives in one place, so it can't drift between environments. Each override changes only what differs. And compose.override.yaml is applied automatically for local dev, so docker compose up "just works" on your laptop.
Profiles
Profiles let optional services stay dormant until you ask for them β perfect for a monitoring or logging stack you don't always need running.
services:
api:
image: ${REGISTRY}/backend:${TAG}
# no profile β always starts
prometheus:
image: prom/prometheus:latest
profiles: [monitoring] # only starts when requested
grafana:
image: grafana/grafana:latest
profiles: [monitoring]
# Core app only
docker compose up -d
# Add the monitoring stack
docker compose --profile monitoring up -d
Production-Critical Features
Health checks and dependency ordering
Health checks let the API wait for a genuinely-ready database rather than a merely-started one:
services:
api:
depends_on:
db:
condition: service_healthy # wait until db passes its health check
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
Restart policies
services:
worker:
deploy:
restart_policy:
condition: on-failure # any | on-failure | none
delay: 5s
max_attempts: 3
window: 120s
Secrets, not environment passwords
Passwords in environment: leak into docker inspect and logs. Compose secrets are mounted as files under /run/secrets/ and kept out of the image and process environment.
services:
api:
environment:
- DB_USER=appuser
- DB_PASSWORD_FILE=/run/secrets/db_password # app reads the file
secrets: [db_password]
secrets:
db_password:
file: ./secrets/db_password.txt
Network segmentation
Put the database on an internal network so it's reachable by the API but has no route to the internet. The web tier sits on a public-facing network; the API bridges both.
networks:
frontend: # public-facing
backend:
internal: true # no external route β DB is hidden
π Key Terms
Internal network: a Docker network with no gateway to the host/internet; containers on it can talk to each other but not out.
Restart policy: the rule for whether and how often Docker restarts a container that exits.
Replica: one of several identical instances of a service, used for load sharing and availability.
Scaling & High Availability
A single Compose host can't survive a machine failure. Docker Swarm turns the same Compose file into a multi-node cluster with built-in load balancing, service discovery, and rolling updates β no new file format to learn.
# Turn the current machine into a Swarm manager
docker swarm init
# Deploy the stack across the cluster
docker stack deploy -c compose.yaml -c compose.prod.yaml myapp
# Scale a service to five replicas
docker service scale myapp_web=5
Zero-downtime rolling updates
Update replicas a few at a time, and automatically roll back if the new version fails its health check:
services:
web:
image: ${REGISTRY}/frontend:${TAG}
deploy:
replicas: 4
update_config:
parallelism: 1 # one at a time
delay: 10s
order: start-first # start new before stopping old
failure_action: rollback
monitor: 60s
Beyond Compose
Compose (with Swarm) covers a lot of ground, but at large scale β complex microservices, multi-region, advanced autoscaling β teams often graduate to Kubernetes. Choose based on your actual needs, not hype.
| Feature | Compose | Swarm | Kubernetes |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Learning curve | Shallow | Moderate | Steep |
| Scalability | Single host | Good | Excellent |
| Auto-healing | Restart policy | Yes | Yes |
| Load balancing | External | Built-in | Built-in |
| Rolling updates | Manual | Yes | Yes |
| Ecosystem | Large | Smaller | Very large |
π‘ Reach for Kubernetes whenβ¦
β¦you have many microservices, need multi-region or multi-cloud, require fine-grained autoscaling, or have complex networking/security policies. Until then, Compose + Swarm keeps operations simple and your cognitive load low.
Hands-on Exercise
ποΈ Productionize a Compose stack
Objective: Convert this development Compose file into a production configuration.
Starting point:
# compose.yaml (development)
services:
web:
build: ./frontend
ports: ["3000:3000"]
volumes:
- ./frontend:/app
environment:
- NODE_ENV=development
command: npm start
api:
build: ./backend
ports: ["4000:4000"]
environment:
- NODE_ENV=development
- DATABASE_URL=mongodb://db:27017/devdb
depends_on: [db]
db:
image: mongo:7
ports: ["27017:27017"]
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
Your task β add:
- Pinned, versioned images via
${REGISTRY}/${TAG} - Resource limits for every service
- Health checks and restart policies
- Secrets for the database password
- A segmented,
internalbackend network
π‘ Hint
Replace build: with image: ${REGISTRY}/web:${TAG}. Move resource settings under deploy.resources.limits. For Mongo, use a CMD-SHELL health check like mongosh --eval "db.adminCommand('ping')", and inject the root password with MONGO_INITDB_ROOT_PASSWORD_FILE.
β Sample solution
# compose.prod.yaml (production)
services:
web:
image: ${REGISTRY}/frontend:${TAG}
deploy:
replicas: 2
resources:
limits: { cpus: '0.5', memory: 512M }
reservations: { cpus: '0.1', memory: 128M }
restart_policy: { condition: on-failure, max_attempts: 3 }
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks: [frontend]
ports: ["80:3000"]
environment:
- NODE_ENV=production
- REACT_APP_API_URL=http://api:4000
api:
image: ${REGISTRY}/backend:${TAG}
deploy:
resources:
limits: { cpus: '1.0', memory: 1G }
restart_policy: { condition: any }
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:4000/health"]
interval: 30s
timeout: 5s
retries: 3
networks: [frontend, backend]
environment:
- NODE_ENV=production
- DATABASE_URL=mongodb://db:27017/proddb
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
depends_on:
db: { condition: service_healthy }
secrets: [db_password]
db:
image: mongo:7
deploy:
resources:
limits: { cpus: '2.0', memory: 2G }
restart_policy: { condition: any }
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --eval \"db.adminCommand('ping')\""]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s
volumes:
- mongodb_data:/data/db
networks: [backend]
environment:
- MONGO_INITDB_ROOT_USERNAME=appuser
- MONGO_INITDB_ROOT_PASSWORD_FILE=/run/secrets/db_password
secrets: [db_password]
networks:
frontend:
backend:
internal: true
volumes:
mongodb_data:
secrets:
db_password:
file: ./secrets/db_password.txt
Deploy it with a small script that sets the tag from git and generates secrets on first run:
#!/usr/bin/env bash
set -euo pipefail
export REGISTRY="registry.example.com"
export TAG="$(git describe --tags --always)"
mkdir -p secrets
[ -f secrets/db_password.txt ] || openssl rand -base64 24 > secrets/db_password.txt
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose ps
π― Quick Quiz
Question 1: What is the benefit of a base + override Compose structure?
Question 2: Why put the database on an internal: true network?
Question 3: When should you consider moving from Compose/Swarm to Kubernetes?
Best Practices
| β Do | β Don't |
|---|---|
| Pin images to a version tag from your registry | Use build: or latest in production |
| Store secrets as Compose secrets or in a vault | Put passwords in environment: or the repo |
Segment networks; mark backend internal | Expose the database port to the host |
| Set resource limits and restart policies | Let a service consume the whole host |
Add health checks and gate depends_on | Assume a started dependency is ready |
Commit a .env.example; keep real secrets out of git | Version-control real credentials |
Summary & Quiz
π Key Takeaways
- Compose runs multi-container apps in production for small-to-medium scale β a lighter alternative to Kubernetes.
- Production configs trade convenience for pinned images, health checks, limits, secrets, and segmented networks.
- Use a base + override structure and profiles to keep config DRY and flexible.
- Docker Swarm scales the same Compose file across a cluster with rolling, zero-downtime updates.
- Graduate to Kubernetes only when scale and complexity truly demand it.
π Further Reading
π What's Next?
You can now build, optimize, and orchestrate containers. Next we zoom out to the platforms that host them: a comparison of the major cloud service providers.
π Orchestration unlocked!
Your whole stack now starts, heals, and updates with one command. Let's find it a home in the cloud.