Compose Intro
Docker Compose runs a multi-container app from one YAML file. docker compose up brings everything online; docker compose down tears it down. The default local-dev tool for any non-trivial stack.
compose.yml, services, volumes, networks
EXAMPLE
# 1) Minimal compose.yml
# (Compose V2 uses compose.yml or docker-compose.yml; V1 uses only docker-compose.yml)
services:
web:
image: nginx:1.27
ports:
- '8080:80'
# Run:
# docker compose up -d # detached
# docker compose down # stop + remove
# docker compose ps # status
# docker compose logs -f # tail logs
# docker compose restart web
# 2) Real-world stack — Postgres + Redis + app
services:
app:
build:
context: .
dockerfile: Dockerfile
target: development # multi-stage build target
ports:
- '3000:3000'
environment:
DATABASE_URL: postgres://app:secret@db:5432/myapp
REDIS_URL: redis://redis:6379
NODE_ENV: development
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
volumes:
- ./src:/app/src # bind mount for hot reload
- /app/node_modules # anonymous volume — preserves container's node_modules
develop:
watch:
- action: sync
path: ./src
target: /app/src
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro
ports:
- '127.0.0.1:5432:5432' # localhost only
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U app']
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports: ['127.0.0.1:6379:6379']
command: ['redis-server', '--maxmemory-policy', 'allkeys-lru']
volumes:
- redisdata:/data
mailhog: # dev-only mail capture
image: mailhog/mailhog:latest
ports:
- '127.0.0.1:1025:1025' # SMTP
- '127.0.0.1:8025:8025' # web UI
volumes:
pgdata: {}
redisdata: {}
# 3) Service discovery
# Within compose, services find each other by NAME:
# db, redis, app, mailhog
# 'db:5432' → resolves to the db container's IP
# No need to know IPs; Docker DNS handles it.
# 4) Build vs image
# image: nginx:1.27 — pull from registry
# build: ./web — build from Dockerfile in ./web
# build:
# context: ./web
# dockerfile: Dockerfile.dev
# target: development — pick a multi-stage stage
# args: # build-time args
# NODE_VERSION: '20'
# 5) Env files — keep secrets out of compose.yml
services:
app:
env_file:
- .env
- .env.local # overrides
environment:
EXTRA_VAR: 'literal'
# .env file gets auto-loaded into 'docker compose' for variable substitution
# .env.local typically gitignored
# Variable substitution
services:
app:
image: myapp:${TAG:-latest} # default if TAG not set
ports: ['${PORT:-3000}:3000']
# 6) Profiles — opt-in services
services:
web: { image: nginx }
db: { image: postgres }
debug:
image: alpine
command: tail -f /dev/null
profiles: ['debug'] # only when explicitly enabled
# docker compose --profile debug up
# Without --profile debug, only web + db start.
# 7) Volumes — three types
volumes:
pgdata: {} # named volume (Docker-managed)
services:
app:
volumes:
- pgdata:/var/lib/postgresql/data # named — survives container restart
- ./code:/app # bind — host directory
- /tmp/app # anonymous — per-container
# Use named for DB data; bind for source code (hot reload); anonymous rarely.
# 8) Networks — isolation
networks:
frontend: {}
backend:
internal: true # no internet access from this network
services:
web: { networks: [frontend] }
api: { networks: [frontend, backend] }
db: { networks: [backend] } # only api can reach db
# 9) Override files — per-environment config
# compose.yml (base)
# compose.override.yml (auto-loaded; dev defaults)
# compose.prod.yml (explicit: -f)
# Dev (default):
docker compose up
# Prod:
docker compose -f compose.yml -f compose.prod.yml up
# 10) Useful commands
docker compose up -d # detached
docker compose up --build # rebuild before up
docker compose down -v # also remove volumes
docker compose ps
docker compose logs -f app
docker compose exec app sh # shell into running service
docker compose run --rm app npm test # one-off command in a NEW container
docker compose restart app
docker compose pause app
docker compose unpause app
docker compose top app
docker compose port app 3000 # which host port is bound
# 11) Health checks — wait for ready, not just started
services:
db:
image: postgres:16
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
retries: 5
app:
depends_on:
db: { condition: service_healthy } # waits for db to be HEALTHY, not just started
# 12) Resource limits (Compose V2)
services:
web:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512m
reservations:
memory: 256m
# 13) Watch mode (Compose V2.22+) — auto-reload on changes
services:
app:
develop:
watch:
- action: sync # sync files to container
path: ./src
target: /app/src
- action: rebuild # rebuild image on package.json change
path: package.json
# docker compose watch
# 14) Logging driver
services:
app:
logging:
driver: json-file
options:
max-size: 10m
max-file: '3'
# 15) Common patterns
# Dev DB seeded from SQL file
services:
db:
image: postgres:16
volumes:
- ./db/seed.sql:/docker-entrypoint-initdb.d/seed.sql:ro
# Background worker
services:
worker:
build: .
command: ['node', 'worker.js']
depends_on: { redis: { condition: service_started } }
restart: unless-stopped
# Reverse proxy in front
services:
proxy:
image: nginx
ports: ['80:80']
depends_on: [api, web]
volumes: ['./nginx.conf:/etc/nginx/nginx.conf:ro']
# 16) Common bugs
# • Forgetting depends_on → app starts before db, crashes
# • depends_on without healthcheck → starts when db PROCESS is up, before it's READY
# • Bind-mount over node_modules → host's node_modules masks container's
# • Same port in two services → 'port already in use'
# • Forgetting to expose internal port → can't connect from host
# • Editing image inside container → lost on restart (changes don't persist)
# 17) Tips
# ✅ Use .env for secrets; gitignore it; commit .env.example
# ✅ Bind localhost: '127.0.0.1:5432' for DBs (don't expose to LAN)
# ✅ Healthchecks → reliable depends_on
# ✅ Profiles for optional services (debug, e2e tests)
# ✅ Override files for per-env config
# ✅ Watch mode for hot reload during dev
# ✅ docker compose down -v when changing volume schema
Why it matters
Docker Compose is the local-dev stack-in-one-file. Use named volumes for state, bind mounts for source, healthchecks + depends_on for boot order, and an .env file for secrets — that’s 90% of any setup.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# Define multi-container apps in YAML. docker compose up docker compose downTry it Yourself »
Discussion
Loading…