Introduction
Docker Compose lets you define and run multi-container applications with a single YAML file. Instead of running docker run commands with many flags, you declare your entire stack โ app, database, cache, reverse proxy โ and start everything with docker compose up.
Prerequisites: Docker and Docker Compose installed. Basic Docker knowledge (see Docker Fundamentals).
Basic docker-compose.yml
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:password@db:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:
# Start all services
docker compose up
# Start in background
docker compose up -d
# Stop all services
docker compose down
# Stop and remove volumes (careful โ deletes data)
docker compose down -v
Essential Commands
# Lifecycle
docker compose up -d # start in background
docker compose down # stop and remove containers
docker compose restart app # restart specific service
docker compose stop # stop without removing
# Logs
docker compose logs # all services
docker compose logs app # specific service
docker compose logs -f app # follow logs
docker compose logs --tail=50 app # last 50 lines
# Exec
docker compose exec app sh # shell in running container
docker compose exec db psql -U postgres myapp # run command
# Build
docker compose build # build all images
docker compose build app # build specific service
docker compose build --no-cache # ignore cache
# Status
docker compose ps # list containers
docker compose top # running processes
docker compose port app 3000 # show mapped port
Networking
Services in the same Compose file can reach each other by service name:
services:
app:
# Can reach 'db' at hostname 'db', port 5432
environment:
DATABASE_URL: postgres://postgres:pass@db:5432/myapp
# NOT localhost:5432 โ that's inside the app container
db:
image: postgres:16-alpine
Custom Networks
services:
frontend:
networks:
- public
- internal
backend:
networks:
- internal
db:
networks:
- internal
# db is NOT accessible from frontend directly
networks:
public:
driver: bridge
internal:
driver: bridge
internal: true # no external access
Volumes
services:
app:
volumes:
# Named volume (managed by Docker, persists across restarts)
- app-data:/app/data
# Bind mount (maps host path to container path)
- ./config:/app/config:ro # :ro = read-only
# Anonymous volume (temporary, removed with container)
- /app/tmp
db:
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
app-data:
postgres-data:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres # store on specific host path
Environment Variables
services:
app:
# Inline values (avoid for secrets)
environment:
NODE_ENV: production
PORT: 3000
# Load from file
env_file:
- .env
- .env.production # overrides .env
# Pass through from host environment
environment:
- AWS_ACCESS_KEY_ID # value comes from host shell
- AWS_SECRET_ACCESS_KEY
# .env file
DATABASE_URL=postgres://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379
SECRET_KEY=your-secret-key
Health Checks
Health checks prevent dependent services from starting before dependencies are ready:
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
interval: 10s # check every 10s
timeout: 5s # fail if no response in 5s
retries: 5 # mark unhealthy after 5 failures
start_period: 30s # grace period before checks start
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
app:
depends_on:
db:
condition: service_healthy # wait for db to be healthy
redis:
condition: service_healthy
Development vs Production Configs
Use multiple Compose files to override settings per environment:
# docker-compose.yml (base)
services:
app:
build: .
environment:
NODE_ENV: production
db:
image: postgres:16-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
# docker-compose.override.yml (development โ auto-loaded)
services:
app:
build:
target: development
volumes:
- .:/app # live code reload
- /app/node_modules # don't override node_modules
environment:
NODE_ENV: development
ports:
- "9229:9229" # Node.js debugger port
db:
ports:
- "5432:5432" # expose DB port for local tools
# docker-compose.prod.yml (production)
services:
app:
image: registry.example.com/myapp:${VERSION}
restart: unless-stopped
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
# Development (uses docker-compose.yml + docker-compose.override.yml automatically)
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Complete Full-Stack Example
# docker-compose.yml โ Node.js + PostgreSQL + Redis + Nginx
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
restart: unless-stopped
app:
build:
context: .
target: production
environment:
NODE_ENV: production
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/myapp
REDIS_URL: redis://redis:6379
SECRET_KEY: ${SECRET_KEY}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
volumes:
postgres-data:
redis-data:
Scaling Services
# Scale app to 3 instances (requires a load balancer like nginx)
docker compose up -d --scale app=3
# Check which ports are mapped
docker compose port app 3000
Troubleshooting
# Container won't start โ check logs
docker compose logs app
# Service can't reach another service
docker compose exec app ping db # test DNS resolution
docker compose exec app nc -zv db 5432 # test TCP connection
# Database connection refused
docker compose exec db pg_isready -U postgres # is postgres ready?
docker compose ps db # is it running?
# Volume permissions
docker compose exec app ls -la /app/data # check ownership
# Rebuild after Dockerfile changes
docker compose build --no-cache app
docker compose up -d app
Comments