Docker Compose Guide: Multi-Container Application Management
Define and manage multi-container applications with Docker Compose: service dependencies, environment variables, health checks, scaling, and production deployment. Comprehensive guide.
Table of Contents
Docker Compose Guide: Multi-Container Application Management
Docker Compose is a tool that lets you define and manage multiple Docker containers using a single YAML file. It's essential for microservice architectures, web applications, and development environments.
What is Docker Compose?
With Docker Compose you can:
- Define multiple services in a single
docker-compose.ymlfile - Manage inter-service dependencies
- Centrally manage network and volume configurations
- Start/stop the entire stack with a single command
Docker Compose v2 runs with the docker compose command (no hyphen) and is integrated into Docker CLI. The legacy docker-compose command is still supported.
Basic docker-compose.yml Structure
version: '3.9'
services:
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret_password
MYSQL_DATABASE: myapp
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
volumes:
db_data:
Services, Volumes and Networks
Defining Services
services:
app:
build:
context: ./app
dockerfile: Dockerfile
image: myapp:latest
container_name: app-container
ports:
- "3000:3000"
environment:
NODE_ENV: production
DB_HOST: database
depends_on:
database:
condition: service_healthy
networks:
- app-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
Defining Volumes
volumes:
# Named volume (managed by Docker)
db_data:
driver: local
# External volume (pre-created)
existing_volume:
external: true
# Custom driver options
nfs_volume:
driver: local
driver_opts:
type: nfs
o: addr=192.168.1.100,rw
device: ":/nfs/share"
Defining Networks
networks:
# Default bridge network
app-network:
driver: bridge
# With custom subnet
custom-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# External network
existing-net:
external: true
depends_on and Healthcheck
Control service startup order with depends_on:
services:
web:
image: nginx:alpine
depends_on:
api:
condition: service_healthy
db:
condition: service_healthy
api:
build: ./api
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
depends_on only controls startup order. Without condition: service_healthy you cannot guarantee the service is ready.
Environment Variables and env_file
Direct Definition
services:
app:
image: myapp:latest
environment:
- NODE_ENV=production
- PORT=3000
- DB_HOST=database
- DB_PORT=5432
Using .env File
services:
app:
image: myapp:latest
env_file:
- .env
- .env.production
.env file:
# .env
NODE_ENV=production
DB_HOST=database
DB_PORT=5432
DB_NAME=myapp
DB_USER=user
DB_PASS=secret_password
JWT_SECRET=very_secret_key
REDIS_URL=redis://redis:6379
Add .env to .gitignore. Never commit sensitive information to Git.
Scaling (--scale)
You can horizontally scale services with Docker Compose:
# Start api service with 3 instances
docker compose up -d --scale api=3
# Change running instance count
docker compose up -d --scale api=5
# Scale a specific service
docker compose scale api=3 worker=2
Load balancer configuration for scaling:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
api:
build: ./api
# Don't define ports - nginx will route
expose:
- "3000"
environment:
NODE_ENV: production
Override Files
Use override files for different environments:
# docker-compose.override.yml (development)
services:
app:
build:
context: .
target: development
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
command: npm run dev
db:
ports:
- "5432:5432" # Expose externally in development
# docker-compose.prod.yml (production)
services:
app:
image: registry.example.com/myapp:latest
restart: always
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 1G
Usage:
# Development (override applied automatically)
docker compose up -d
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Production Best Practices
Full Production Example
version: '3.9'
services:
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot_data:/etc/letsencrypt:ro
depends_on:
- api
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
api:
image: registry.example.com/api:${APP_VERSION:-latest}
env_file: .env.production
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: always
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
db:
image: postgres:15-alpine
env_file: .env.production
volumes:
- postgres_data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
volumes:
postgres_data:
redis_data:
certbot_data:
Essential Commands
# Start all services
docker compose up -d
# Start specific service
docker compose up -d api
# Follow logs
docker compose logs -f
docker compose logs -f api
# Stop services
docker compose down
# Stop services and remove volumes
docker compose down -v
# Restart a service
docker compose restart api
# View service status
docker compose ps
# Execute command in service
docker compose exec api bash
# Rebuild images
docker compose build --no-cache
docker compose up -d --build
# Validate configuration
docker compose config
Conclusion
Docker Compose is the most practical way to manage multi-container applications. It provides a consistent environment from development to production. With override files you can flexibly configure different environments, and with healthcheck and depends_on you can safely manage service dependencies.
Related Articles
Automated Deployment with GitHub Actions: CI/CD Pipeline Setup
Build CI/CD pipelines with GitHub Actions: Docker build and push, SSH server deployment, test automation, secrets management, and workflow optimization.
Server Automation with Ansible: Infrastructure as Code
Ansible installation, inventory file, playbook writing, role structure, variables, encrypted vault, ad-hoc commands, and server configuration automation. Step-by-step guide.
Caddy Web Server Setup: Automatic HTTPS and Reverse Proxy
Caddy web server installation, Caddyfile configuration, automatic Let's Encrypt SSL, reverse proxy, static site hosting, PHP-FPM integration, and performance settings.