Docker Compose: Multi-Container Applications Made Easy (2026)
Real applications don't run in a single container. Docker Compose orchestrates multiple services together — here's how to use it like a pro.
The Basics: docker-compose.yml
# docker-compose.yml — the single source of truth for your app
version: "3.8"
services:
# Service 1: Your application
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapp-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:secret@db:5432/mydb
- REDIS_URL=redis://redis:6379
volumes:
- .:/app # Live reload during development
- /app/node_modules # Don't override node_modules
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# Service 2: PostgreSQL database
db:
image: postgres:16-alpine
container_name: myapp-db
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data # Persist data across container restarts
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # Init script on first run
ports:
- "5432:5432" # Comment out in production!
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
# Service 3: Redis cache
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
ports:
- "6379:6379" # Comment out in production!
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Named volumes (persist data even when containers are removed)
volumes:
pgdata:
driver: local
redisdata:
driver: local
# Custom network (services can communicate by service name!)
networks:
app-network:
driver: bridge
Essential Commands
# === Lifecycle Commands ===
docker compose up -d # Start all services (detached/background mode)
docker compose down # Stop and remove all containers + networks
docker compose down -v # Also remove named volumes (⚠️ data loss!)
# === Development vs Production ===
# Development (with hot-reload):
docker compose up # Foreground (see logs live)
# Production (detached):
docker compose up -d --build # Build if needed, then start backgrounded
# === Managing Individual Services ===
docker compose up -d db redis # Start only specific services
docker compose stop app # Stop one service (others keep running)
docker compose restart app # Restart one service
docker compose logs app # View logs for one service
docker compose logs -f --tail=50 app # Follow last 50 lines of app logs
docker compose logs -f # Follow ALL services' logs interleaved
# === Building & Images ===
docker compose build # Rebuild all services
docker compose build app # Rebuild only app service
docker compose build --no-cache # Force rebuild without layer cache
# === Inside Containers ===
docker compose exec app sh # Open shell inside running app container
docker compose exec db psql -U app mydb # Connect to Postgres directly
docker compose exec redis redis-cli # Connect to Redis CLI
# === Status & Debugging ===
docker compose ps # List all containers with status
docker compose top # Running processes in each container
docker compose config # Validate and view resolved config
docker compose images # Show images used by services
docker compose port app 3000 # Map which host port maps to container port 3000
# === Cleanup ===
docker system prune -a # Remove unused images, networks, builders
docker volume prune # Remove unused volumes
Development Workflow
# docker-compose.dev.yml — Development overrides
# Use: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
version: "3.8"
services:
app:
build:
context: .
target: development # Multi-stage build dev stage
environment:
- NODE_ENV=development
- DEBUG=app:*
volumes:
- .:/app # Live code sync
- /app/node_modules
command: npm run dev # Watch mode / nodemon
ports:
- "9229:9229" # Node.js debugger port
db:
ports:
- "5432:5432" # Expose DB for local tools (pgAdmin, DBeaver)
# Add development-only service:
adminer:
image: adminer:latest
ports:
- "8080:8080" # Web-based DB admin UI at localhost:8080
# docker-compose.prod.yml — Production overrides
# Use: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
version: "3.8"
services:
app:
image: myapp:${VERSION:-latest} # Use pre-built image from registry
restart: always
environment:
- NODE_ENV=production
volumes:
- ./logs:/app/logs # Only persist logs, not code
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} # From .env file or secrets manager
volumes:
- production_pgdata:/var/lib/postgresql/data
deploy:
resources:
limits:
memory: 1G
redis:
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
Common Patterns
# === Pattern 1: Nginx Reverse Proxy ===
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
networks:
- frontend
app:
# ... no exposed ports! Nginx handles all external traffic
networks:
- frontend
- backend
# nginx.conf upstream block:
# upstream app_backend {
# server app:3000;
# }
# server {
# listen 80;
# server_name example.com;
# location / {
# proxy_pass http://app_backend;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# }
# === Pattern 2: Background Worker ===
services:
worker:
build: .
command: npm run worker # Separate process for queue jobs
restart: unless-stopped
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=redis://redis:6379
depends_on:
redis:
condition: service_healthy
deploy:
replicas: 2 # Run 2 worker instances!
networks:
- app-network
# Scale workers based on load:
# docker compose up -d --scale worker=4
# === Pattern 3: One-off Jobs (migrations, seeds) ===
# Run once and exit:
# docker compose run --rm app npm run migrate
# docker compose run --rm app npm run seed
# docker compose exec db pg_dump -U app mydb > backup.sql
# === Pattern 4: Health Check Dependent Startup ===
services:
app:
depends_on:
db:
condition: service_healthy # Wait until DB is ACTUALLY ready
redis:
condition: service_healthy
# This is CRITICAL — containers start in parallel by default.
# Without health checks, app might try to connect before DB accepts connections!
# === Pattern 5: Secrets Management ===
# Option A: .env file (never commit to git!)
# echo "DB_PASSWORD=supersecret" > .env
# .env is loaded automatically by docker compose
# Option B: Docker secrets (Swarm mode):
# echo "my_password" | docker secret create db_password -
# In compose:
# environment:
# - DB_PASSWORD_FILE=/run/secrets/db_password
# secrets:
# - db_password
# secrets:
# db_password:
# external: true
# Option C: External secrets manager (recommended for production):
# environment:
# - DB_PASSWORD=${DB_PASSWORD} # From CI/CD or vault
Troubleshooting Guide
# Container won't start?
docker compose logs app # Check logs first!
docker compose ps # Is it actually running?
docker compose exec app env # Are env vars correct?
# Port already in use?
sudo lsof -i :3000 # What's using the port?
# Change mapping: "3001:3000"
# Database connection refused?
docker compose logs db # Is DB running?
docker compose exec db psql -U app -c 'SELECT 1' # Can we connect to DB?
# Check: is app trying to connect to "localhost:5432" instead of "db:5432"?
# In compose, use SERVICE name as hostname, NOT localhost!
# Volume has old data?
docker compose down -v # Remove volumes and start fresh
# ⚠️ This deletes all persisted data!
# Image won't rebuild?
docker compose build --no-cache app # Clear build cache
docker builder prune # Remove ALL build cache
# Container runs out of space?
docker system df # See disk usage
docker system prune -a # Clean up unused images
# Network issues between containers?
docker network ls # List networks
docker network inspect app_network # Check if services are on same network
# All services in same `networks:` list can reach each other by service name
What's your favorite Docker Compose trick? What multi-container setup are you running?
Follow @armorbreak for more practical developer guides.
Top comments (0)