Container Orchestration and Docker Compose
Managing multiple containers manually becomes unwieldy fast. I learned this the hard way when I had to coordinate a web server, database, cache, and background workers across different environments. Docker Compose solved this by letting me define entire application stacks in a single file.
Understanding Multi-Container Applications
Modern applications rarely run in isolation. A typical web application might include:
- Web server (frontend)
- API server (backend)
- Database (PostgreSQL, MySQL)
- Cache (Redis, Memcached)
- Message queue (RabbitMQ, Apache Kafka)
Coordinating these services manually means remembering port mappings, network configurations, environment variables, and startup order. Docker Compose eliminates this complexity.
Docker Compose Fundamentals
Docker Compose uses YAML files to define services, networks, and volumes:
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/myapp
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
This defines three services that can communicate with each other using service names as hostnames. The web service can connect to the database using db:5432
and Redis using redis:6379
.
Service Configuration and Dependencies
The depends_on
directive controls startup order, but it doesn’t wait for services to be ready. For applications that need the database to be fully initialized:
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
The healthcheck ensures the database is accepting connections before starting the web service. I always include health checks for critical services like databases.
Networking in Docker Compose
Docker Compose automatically creates a network for your services. You can also define custom networks for better isolation:
version: '3.8'
services:
frontend:
build: ./frontend
networks:
- frontend-net
ports:
- "3000:3000"
api:
build: ./api
networks:
- frontend-net
- backend-net
db:
image: postgres:15-alpine
networks:
- backend-net
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
frontend-net:
backend-net:
volumes:
postgres_data:
This setup isolates the database on the backend network, preventing the frontend from directly accessing it.
Environment Configuration
Managing configuration across different environments is crucial. I use multiple compose files and environment files:
# docker-compose.yml (base configuration)
version: '3.8'
services:
web:
build: .
environment:
- NODE_ENV=${NODE_ENV:-development}
env_file:
- .env
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# docker-compose.override.yml (development overrides)
version: '3.8'
services:
web:
ports:
- "8000:8000"
volumes:
- ./src:/app/src
command: npm run dev
db:
ports:
- "5432:5432"
Use different configurations with:
# Development (uses docker-compose.override.yml automatically)
docker-compose up
# Production
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
Development Workflow Optimization
For development, I optimize for fast feedback loops and easy debugging:
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
volumes:
- ./src:/app/src
- /app/node_modules # Anonymous volume to preserve node_modules
environment:
- NODE_ENV=development
- CHOKIDAR_USEPOLLING=true # For file watching in containers
command: npm run dev
db:
image: postgres:15-alpine
ports:
- "5432:5432" # Expose for external tools
environment:
- POSTGRES_DB=myapp_dev
- POSTGRES_USER=dev
- POSTGRES_PASSWORD=devpass
volumes:
- postgres_dev_data:/var/lib/postgresql/data
volumes:
postgres_dev_data:
Common Patterns and Troubleshooting
I’ve encountered these issues repeatedly and learned to avoid them:
Problem: Services can’t communicate
# Check if services are on the same network
docker-compose ps
docker network ls
Problem: Database connection refused
# Add health checks and proper depends_on
services:
web:
depends_on:
db:
condition: service_healthy
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
retries: 5
Problem: File changes not reflected in development
# Ensure proper volume mounting and file watching
services:
web:
volumes:
- ./src:/app/src
environment:
- CHOKIDAR_USEPOLLING=true
Docker Compose provides an excellent foundation for understanding container orchestration concepts. The skills you learn here translate directly to Kubernetes, which I’ll cover in the next part along with production-grade orchestration strategies.