Docker Compose and Multi-Container Applications
Docker Compose simplifies the management of multi-container applications by allowing you to define and run complex applications using a single YAML file. This section covers Compose fundamentals, advanced patterns, and real-world application examples.
Introduction to Docker Compose
What is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services, networks, and volumes, then create and start all services with a single command.
Installing Docker Compose
# Docker Compose comes with Docker Desktop
# For Linux, install separately:
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Verify installation
docker-compose --version
Basic Compose File Structure
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html:ro
database:
image: postgres:13
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=secret
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
networks:
default:
driver: bridge
Compose File Deep Dive
Service Configuration
version: '3.8'
services:
web:
# Build from Dockerfile
build:
context: ./web
dockerfile: Dockerfile.prod
args:
- NODE_ENV=production
- API_URL=http://api:3000
# Or use pre-built image
image: my-web-app:latest
# Container name
container_name: web-server
# Restart policy
restart: unless-stopped
# Port mapping
ports:
- "80:80"
- "443:443"
# Environment variables
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
# Environment file
env_file:
- .env
- .env.production
# Volume mounts
volumes:
- ./config:/etc/nginx/conf.d:ro
- web_data:/var/www/html
- /var/run/docker.sock:/var/run/docker.sock:ro
# Network configuration
networks:
- frontend
- backend
# Dependencies
depends_on:
- database
- cache
# Resource limits
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Advanced Service Options
services:
app:
build: .
# Command override
command: ["npm", "run", "start:prod"]
# Working directory
working_dir: /app
# User specification
user: "1000:1000"
# Hostname
hostname: app-server
# DNS configuration
dns:
- 8.8.8.8
- 8.8.4.4
# Extra hosts
extra_hosts:
- "api.local:192.168.1.100"
- "db.local:192.168.1.101"
# Security options
security_opt:
- no-new-privileges:true
# Capabilities
cap_add:
- NET_ADMIN
cap_drop:
- ALL
# Logging configuration
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Real-World Application Examples
Example 1: Full-Stack Web Application
# docker-compose.yml
version: '3.8'
services:
# Frontend (React)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
container_name: react-frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- frontend-net
restart: unless-stopped
# Backend API (Node.js)
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: node-backend
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:password@postgres:5432/myapp
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
- postgres
- redis
networks:
- frontend-net
- backend-net
volumes:
- ./uploads:/app/uploads
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# Database (PostgreSQL)
postgres:
image: postgres:13-alpine
container_name: postgres-db
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend-net
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 30s
timeout: 10s
retries: 5
# Cache (Redis)
redis:
image: redis:6-alpine
container_name: redis-cache
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- backend-net
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Reverse Proxy (Nginx)
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- frontend
- backend
networks:
- frontend-net
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
Example 2: Microservices Architecture
# docker-compose.microservices.yml
version: '3.8'
services:
# API Gateway
gateway:
build: ./gateway
ports:
- "80:8080"
environment:
- USER_SERVICE_URL=http://user-service:3000
- ORDER_SERVICE_URL=http://order-service:3000
- PRODUCT_SERVICE_URL=http://product-service:3000
depends_on:
- user-service
- order-service
- product-service
networks:
- microservices-net
# User Service
user-service:
build: ./services/user
environment:
- DATABASE_URL=postgresql://user:pass@user-db:5432/users
- REDIS_URL=redis://redis:6379
depends_on:
- user-db
- redis
networks:
- microservices-net
- user-db-net
deploy:
replicas: 2
user-db:
image: postgres:13-alpine
environment:
- POSTGRES_DB=users
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- user_db_data:/var/lib/postgresql/data
networks:
- user-db-net
# Order Service
order-service:
build: ./services/order
environment:
- DATABASE_URL=postgresql://order:pass@order-db:5432/orders
- USER_SERVICE_URL=http://user-service:3000
depends_on:
- order-db
networks:
- microservices-net
- order-db-net
order-db:
image: postgres:13-alpine
environment:
- POSTGRES_DB=orders
- POSTGRES_USER=order
- POSTGRES_PASSWORD=pass
volumes:
- order_db_data:/var/lib/postgresql/data
networks:
- order-db-net
# Product Service
product-service:
build: ./services/product
environment:
- MONGODB_URI=mongodb://product-db:27017/products
depends_on:
- product-db
networks:
- microservices-net
- product-db-net
product-db:
image: mongo:5.0
volumes:
- product_db_data:/data/db
networks:
- product-db-net
# Shared Redis Cache
redis:
image: redis:6-alpine
volumes:
- redis_data:/data
networks:
- microservices-net
# Message Queue
rabbitmq:
image: rabbitmq:3-management
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=secret
ports:
- "15672:15672" # Management UI
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- microservices-net
volumes:
user_db_data:
order_db_data:
product_db_data:
redis_data:
rabbitmq_data:
networks:
microservices-net:
driver: bridge
user-db-net:
driver: bridge
internal: true
order-db-net:
driver: bridge
internal: true
product-db-net:
driver: bridge
internal: true
Example 3: Development Environment
# docker-compose.dev.yml
version: '3.8'
services:
# Development API with hot reload
api:
build:
context: ./api
dockerfile: Dockerfile.dev
volumes:
- ./api:/app
- /app/node_modules
ports:
- "3000:3000"
- "9229:9229" # Debug port
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://dev:dev@postgres:5432/devdb
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
command: npm run dev:debug
# Frontend with hot reload
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "3001:3000"
environment:
- REACT_APP_API_URL=http://localhost:3000
- CHOKIDAR_USEPOLLING=true
command: npm start
# Development database
postgres:
image: postgres:13-alpine
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=dev
- POSTGRES_PASSWORD=dev
ports:
- "5432:5432" # Expose for external tools
volumes:
- postgres_dev_data:/var/lib/postgresql/data
- ./database/init:/docker-entrypoint-initdb.d
# Development Redis
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- redis_dev_data:/data
# Development tools
adminer:
image: adminer:latest
ports:
- "8080:8080"
depends_on:
- postgres
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- "8081:8081"
depends_on:
- redis
volumes:
postgres_dev_data:
redis_dev_data:
Docker Compose Commands
Basic Operations
# Start services
docker-compose up
docker-compose up -d # Detached mode
# Start specific services
docker-compose up web database
# Build and start
docker-compose up --build
# Stop services
docker-compose stop
docker-compose down # Stop and remove containers
# Stop and remove everything (including volumes)
docker-compose down -v
# Restart services
docker-compose restart
docker-compose restart web # Restart specific service
Service Management
# Scale services
docker-compose up -d --scale web=3 --scale api=2
# View running services
docker-compose ps
# View logs
docker-compose logs
docker-compose logs -f web # Follow logs for specific service
docker-compose logs --tail=100 api
# Execute commands in services
docker-compose exec web /bin/bash
docker-compose exec database psql -U user -d myapp
# Run one-off commands
docker-compose run --rm web npm test
docker-compose run --rm database pg_dump -U user myapp > backup.sql
Configuration Management
# Validate compose file
docker-compose config
# View resolved configuration
docker-compose config --services
docker-compose config --volumes
# Use different compose files
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
# Override with environment-specific files
docker-compose -f docker-compose.yml -f docker-compose.override.yml up
Environment-Specific Configurations
Development Override
# docker-compose.override.yml (automatically loaded)
version: '3.8'
services:
web:
build:
target: development
volumes:
- ./src:/app/src
environment:
- NODE_ENV=development
- DEBUG=true
ports:
- "3000:3000"
- "9229:9229" # Debug port
database:
ports:
- "5432:5432" # Expose for development tools
environment:
- POSTGRES_DB=devdb
Production Configuration
# docker-compose.prod.yml
version: '3.8'
services:
web:
build:
target: production
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
database:
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
volumes:
- /opt/postgres/data:/var/lib/postgresql/data
Using Environment Files
# .env file
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=myapp
API_KEY=your-api-key-here
NODE_ENV=production
# docker-compose.yml
version: '3.8'
services:
web:
image: my-app:latest
environment:
- NODE_ENV=${NODE_ENV}
- API_KEY=${API_KEY}
database:
image: postgres:13
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
Advanced Compose Patterns
Health Checks and Dependencies
services:
database:
image: postgres:13
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
web:
image: my-app:latest
depends_on:
database:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
Secrets Management
# docker-compose.yml
version: '3.8'
services:
web:
image: my-app:latest
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true
Multi-Stage Builds with Compose
# Dockerfile
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:16-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
FROM node:16-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["npm", "start"]
# docker-compose.yml
services:
web:
build:
context: .
target: ${BUILD_TARGET:-production}
Monitoring and Logging
Centralized Logging
# docker-compose.logging.yml
version: '3.8'
services:
web:
image: my-app:latest
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: web.app
elasticsearch:
image: elasticsearch:7.14.0
environment:
- discovery.type=single-node
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
fluentd:
build: ./fluentd
volumes:
- ./fluentd/conf:/fluentd/etc
ports:
- "24224:24224"
depends_on:
- elasticsearch
kibana:
image: kibana:7.14.0
ports:
- "5601:5601"
depends_on:
- elasticsearch
volumes:
elasticsearch_data:
Monitoring Stack
# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/datasources:/etc/grafana/provisioning/datasources
node-exporter:
image: prom/node-exporter:latest
ports:
- "9100:9100"
volumes:
prometheus_data:
grafana_data:
Best Practices
Compose File Organization
# Project structure
project/
├── docker-compose.yml # Base configuration
├── docker-compose.override.yml # Development overrides
├── docker-compose.prod.yml # Production configuration
├── docker-compose.test.yml # Testing configuration
├── .env # Environment variables
├── .env.example # Environment template
└── services/
├── web/
│ ├── Dockerfile
│ └── src/
├── api/
│ ├── Dockerfile
│ └── src/
└── database/
└── init/
Security Best Practices
services:
web:
image: my-app:latest
user: "1000:1000" # Non-root user
read_only: true # Read-only filesystem
tmpfs:
- /tmp
- /var/cache
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
Summary
In this section, you learned:
Docker Compose Fundamentals
- Compose file structure and syntax
- Service configuration options
- Volume and network management
- Environment-specific configurations
Real-World Applications
- Full-stack web application setup
- Microservices architecture patterns
- Development environment configuration
- Production deployment considerations
Advanced Patterns
- Health checks and service dependencies
- Secrets management
- Multi-stage builds with Compose
- Monitoring and logging integration
Best Practices
- Project organization and file structure
- Security configurations
- Environment management
- Service scaling and resource limits
Key Takeaways:
- Compose simplifies multi-container application management
- Use override files for environment-specific configurations
- Implement health checks for reliable service dependencies
- Always consider security when configuring services
- Monitor and log your applications for production readiness
Next, we’ll explore production deployment strategies, security best practices, and performance optimization techniques for Docker applications.