Docker Compose: Multi-Container Applications Made Easy (2026)
Real apps 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
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:password@db:5432/mydb
- REDIS_URL=redis://cache:6379
volumes:
- .:/app
- /app/node_modules # Anonymous volume prevents host overwriting
depends_on:
- db
- cache
networks:
- app-network
# Service 2: PostgreSQL database
db:
image: postgres:16-alpine
container_name: myapp-db
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # Auto-run on first start
ports:
- "5432:5432"
networks:
- app-network
# Service 3: Redis cache
cache:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
volumes:
postgres_data: # Named volume — persists across container recreation
redis_data:
networks:
app-network:
driver: bridge # All services can communicate by service name
Essential Commands
# Start everything (build if needed):
docker compose up -d # -d = detached (background)
docker compose up -d --build # Force rebuild even if image exists
# Stop and remove containers:
docker compose down # Stops + removes containers + network
docker compose down -v # Also removes named volumes (⚠️ data loss!)
# View status:
docker compose ps # List running services
docker compose logs # View all logs
docker compose logs -f app # Follow logs for specific service
docker compose logs --tail=50 # Last 50 lines
# Execute commands inside container:
docker compose exec app sh # Interactive shell in app container
docker compose exec db psql -U app # Connect to Postgres directly
# Restart single service:
docker compose restart cache
# Scale services (for stateless ones):
docker compose up -d --scale app=3 # Run 3 instances of app
# Check resource usage:
docker compose top # Process list per container
docker stats # Live CPU/Memory usage
# Clean up unused resources:
docker system prune -a # Remove all stopped images, containers, networks
Production Patterns
# Pattern 1: Health checks (essential for orchestration!)
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
# Pattern 2: Environment variables from file (.env)
services:
app:
env_file:
- .env
environment:
# Override or add specific vars
- PORT=${PORT:-3000}
# Pattern 3: Multi-stage build (smaller images)
# Dockerfile:
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm run build
FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Pattern 4: Nginx reverse proxy
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
networks:
- app-network
# nginx.conf:
# upstream app_server {
# server app:3000;
# }
# server {
# listen 80;
# location / {
# proxy_pass http://app_server;
# 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 5: Secrets management
secrets:
db_password:
file: ./secrets/db_password.txt
jwt_secret:
external: true # Created with: docker secret create jwt_secret ...
services:
app:
secrets:
- db_password
- jwt_secret
Common Scenarios
# Scenario 1: Add a background worker
services:
worker:
build: .
container_name: myapp-worker
command: npm run worker # Different entrypoint!
environment:
- DATABASE_URL=postgres://app:password@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
deploy:
replicas: 2 # Run 2 worker instances
networks:
- app-network
# Scenario 2: Development vs Production overrides
# Base: docker-compose.yml (shared config)
# Dev override: docker-compose.override.yml (auto-loaded with `docker compose up`)
# Prod: docker-compose.prod.yml (`docker compose -f docker-compose.yml -f docker-compose.prod.yml up`)
# docker-compose.override.yml (dev only):
services:
app:
volumes:
- .:/app # Hot reload via bind mount
environment:
- NODE_ENV=development
db:
ports:
- "5432:5432" # Expose DB port locally only in dev
# Scenario 3: One-command setup for new developers
# Just: git clone && cp .env.example .env && docker compose up -d
# That's it. No Node.js, no Postgres, no Redis installation needed.
What's your favorite Docker Compose trick? How do you handle local development?
Follow @armorbreak for more practical developer guides.
Top comments (0)