docker
Docker Compose: Multi-Container Applications
Beginner
13 minutes
October 2026
CloudOpsGuide Team
Docker Compose: Multi-Container Applications
Define and run multi-container Docker applications — from basic setup to production patterns with volumes, networks, and health checks.
Table of Contents
- Compose File Basics
- Services
- Volumes and Persistence
- Networking
- Environment Variables
- Health Checks and Dependencies
- Production Patterns
- Common Commands
Compose File Basics
# docker-compose.yml
services:
web:
image: nginx:alpine
ports:
- "80:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
docker compose up -d
docker compose down
docker compose logs -f
The
versionfield is deprecated in modern Compose — omit it.
Services
Full Service Definition
services:
api:
build:
context: ./api
dockerfile: Dockerfile
image: my-api:latest
container_name: my-api
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgres://db:5432/myapp
depends_on:
db:
condition: service_healthy
volumes:
- ./api:/app # dev hot-reload
- api-logs:/logs # named volume
networks:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
Build vs Image
# Option 1: Pre-built image
services:
app:
image: myregistry.com/app:1.2.0
# Option 2: Build from Dockerfile
services:
app:
build: .
image: my-app:dev
Volumes and Persistence
Named Volumes
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: # Docker manages location
Bind Mounts
services:
app:
volumes:
- ./src:/app/src # relative to compose file
- /opt/app/logs:/var/log # absolute host path
Read-Only Mounts
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
Networking
Default Network
All services in a compose file share a default network. Services reach each other by name:
services:
web:
image: myapp
environment:
DB_HOST: db # ← service name resolves to IP
db:
image: postgres:16
Custom Networks
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net # API bridges both
db:
networks:
- backend-net # isolated from frontend
networks:
frontend-net:
backend-net:
Aliases
services:
api:
networks:
backend:
aliases:
- api.internal
Environment Variables
Inline
environment:
- NODE_ENV=production
- LOG_LEVEL=info
.env File
# .env
DATABASE_URL=postgres://db:5432/myapp
API_KEY=abc123
services:
app:
env_file:
- .env
Compose Variable Substitution
services:
app:
image: myapp:${TAG:-latest}
TAG=v1.2.0 docker compose up
Health Checks and Dependencies
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
api:
depends_on:
db:
condition: service_healthy # wait for healthy, not just started
redis:
condition: service_started # just wait for start
Conditions: service_started (default), service_healthy, service_completed_successfully.
Production Patterns
Restart Policies
services:
app:
restart: unless-stopped # restart unless manually stopped
# Other options: no, always, on-failure
Resource Limits
services:
app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
Multi-Environment Override
# docker-compose.yml — base
# docker-compose.prod.yml — prod overrides
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# docker-compose.prod.yml
services:
app:
restart: always
environment:
LOG_LEVEL: warn
Common Commands
# Start all services (detached)
docker compose up -d
# View logs (follow)
docker compose logs -f api
# Scale a service
docker compose up -d --scale worker=3
# Rebuild and restart one service
docker compose up -d --build api
# Stop everything
docker compose down
# Stop + remove volumes (destroys data)
docker compose down -v
# See running services
docker compose ps
# Execute command in running container
docker compose exec db psql -U postgres
# View resolved config (after env substitution)
docker compose config
Common Mistakes
1. Port Conflicts
# Both map to host port 80 — second one fails
services:
web1: { ports: ["80:80"] }
web2: { ports: ["80:80"] } # ← conflict
# Fix: use different host ports
web2: { ports: ["8080:80"] }
2. Secrets in Environment
# Bad — visible in docker inspect
environment:
DB_PASSWORD: supersecret
# Better — use secrets or .env
env_file:
- .env # add .env to .gitignore
3. Depending on Service Started vs Healthy
# Bad — api starts before db is ready
depends_on:
- db
# Good — waits for health check
depends_on:
db:
condition: service_healthy
Related Articles
Last Updated: October 2026
Author: CloudOpsGuide Team
Difficulty: Beginner
Estimated Reading Time: 13 minutes