compose.yaml
docker-compose.yml describes a multi-container app declaratively: services, networks, volumes, depends_on, healthchecks, profiles. Compose v2 (built into Docker Desktop and the CLI plugin) is the modern target — the v1 standalone tool is deprecated. The same file shape works for local dev, CI, and small single-host deploys.
A production-shaped compose file with health and profiles
EXAMPLE
# compose.yaml (Compose v2 — no 'version:' key needed)
name: shop
services:
api:
image: ${IMAGE:-shop/api:dev}
build:
context: ./api
target: ${BUILD_TARGET:-runtime}
cache_from: ['shop/api:latest']
ports: ['3000:3000']
environment:
NODE_ENV: ${NODE_ENV:-development}
DATABASE_URL: postgres://shop:${DB_PASSWORD}@db:5432/shop
REDIS_URL: redis://redis:6379
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
healthcheck:
test: ['CMD', 'curl', '-fsS', 'http://localhost:3000/health']
interval: 10s
timeout: 2s
retries: 5
start_period: 30s
restart: unless-stopped
deploy:
resources:
limits: { cpus: '1.0', memory: 768M }
reservations: { cpus: '0.25', memory: 256M }
logging:
driver: 'json-file'
options: { max-size: '20m', max-file: '5' }
networks: [internal, web]
worker:
image: ${IMAGE:-shop/api:dev}
command: ['node', 'dist/worker.js']
environment:
DATABASE_URL: postgres://shop:${DB_PASSWORD}@db:5432/shop
REDIS_URL: redis://redis:6379
depends_on: { db: { condition: service_healthy }, redis: { condition: service_healthy } }
restart: unless-stopped
networks: [internal]
profiles: ['workers'] # only started when 'docker compose --profile workers up'
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: shop
POSTGRES_USER: shop
POSTGRES_PASSWORD: ${DB_PASSWORD:?required}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U shop -d shop']
interval: 5s
timeout: 2s
retries: 10
networks: [internal]
redis:
image: redis:7-alpine
command: ['redis-server', '--save', '60', '1', '--loglevel', 'warning']
volumes: [redisdata:/data]
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
networks: [internal]
proxy:
image: nginx:1.27-alpine
ports: ['80:80']
volumes: ['./nginx.conf:/etc/nginx/conf.d/default.conf:ro']
depends_on: { api: { condition: service_healthy } }
networks: [web]
networks:
web: {} # public-ish: exposed via 'proxy'
internal: { internal: true } # no path to public internet
volumes:
pgdata: {}
redisdata: {}
# ----------------- Usage -----------------
# docker compose up -d # start the default services
# docker compose --profile workers up -d # also start 'worker'
# docker compose ps # what's running + healthcheck status
# docker compose logs -f api # tail
# docker compose exec api sh # shell into a container
# docker compose down # stop + remove (KEEPS named volumes)
# docker compose down -v # ALSO removes volumes (dangerous)
# Multiple files (override patterns)
# docker compose -f compose.yaml -f compose.prod.yaml up -d
# Files merge in order; later wins. Standard pattern: base compose + dev/prod override.
# .env file is auto-loaded; use ${VAR:?required} to fail fast when missing.
# Resolve interpolation: docker compose config
Why it matters
Use `depends_on` with `condition: service_healthy` instead of bare ordering. Containers start in dependency order, but only the healthcheck tells Compose when the dependency is actually ready to serve — the difference between "the API starts and crashes because Postgres has not finished initialising" and "the API starts only after Postgres is accepting connections".
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
services:
api:
build: .
ports: ["3000:3000"]
depends_on: [db]
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Try it Yourself »
Discussion
Loading…