Services & Dependencies
Each service in a compose file is a container template plus the networking, mounts, healthchecks, and dependencies it needs. The shape is more important than the syntax: name services after the role they play, not the image; declare healthchecks; make dependencies explicit; bind public ports sparingly.
A multi-service compose layout, in detail
EXAMPLE
# compose.yaml — production-ish shape for a small stack
name: shop
services:
proxy:
image: nginx:1.27-alpine
ports: ['80:80', '443:443']
volumes:
- ./infra/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./infra/certs:/etc/nginx/certs:ro
depends_on:
api: { condition: service_healthy }
networks: [web, internal]
restart: unless-stopped
deploy:
resources: { limits: { cpus: '0.5', memory: 128M } }
api:
image: ${IMAGE_API:-shop/api:dev}
build: { context: ./api, target: ${BUILD_TARGET:-runtime} }
environment:
NODE_ENV: ${NODE_ENV:-production}
DATABASE_URL: postgres://shop:${DB_PASSWORD}@db:5432/shop
REDIS_URL: redis://redis:6379
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY?required}
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
networks: [internal]
restart: unless-stopped
deploy:
resources:
limits: { cpus: '1.0', memory: 768M }
reservations: { cpus: '0.25', memory: 256M }
worker:
image: ${IMAGE_API:-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 } }
networks: [internal]
restart: unless-stopped
profiles: ['workers'] # only started with '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
- ./db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U shop -d shop']
interval: 5s
timeout: 2s
retries: 10
networks: [internal]
# DB never gets a port mapping in prod — only the app can reach it
redis:
image: redis:7-alpine
command:
- 'redis-server'
- '--save'
- '60 1'
- '--loglevel'
- 'warning'
volumes: [redisdata:/data]
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 2s
retries: 5
networks: [internal]
networks:
web:
driver: bridge
internal:
driver: bridge
internal: true # NO route to the public internet
volumes:
pgdata: {}
redisdata: {}
# ---------------- Patterns to internalise ----------------
# 1) Service names ARE DNS names on the network
# 'redis' resolves automatically; never hardcode IPs.
# 2) depends_on + condition: service_healthy
# Pure order is not enough — the dependency must be READY.
# 3) Healthchecks at the SERVICE level
# The container itself decides 'I am ready' (HTTP /health, pg_isready, ping).
# Compose blocks dependent services until then.
# 4) Network segmentation
# 'internal: true' kills the path to the public internet; only 'proxy'
# has it via the 'web' network. Lateral movement is contained.
# 5) Resource limits
# 'deploy.resources.limits' enforces cpu/memory budgets even in compose
# (the swarm-style key works in plain compose v2 too).
# 6) Profiles
# 'docker compose --profile workers up' to start optional services.
# Keeps the default 'compose up' tidy.
# 7) Required env vars
# DB_PASSWORD?required fails fast if you forget to set it.
# 8) Override files for environments
# compose.yaml + compose.dev.yaml + compose.prod.yaml; merged left to right.
# 9) NEVER expose the DB on a host port in production
# Only the proxy and the API need to reach the DB; they do via 'internal'.
# 10) Pin image tags
# Use 'redis:7-alpine' not 'redis:latest'; reproducible builds + safer upgrades.
Why it matters
Treat the compose file like a tiny topology diagram: every service is a role, every network is a security boundary, every volume is durable state. Start with healthchecks, no host port mappings beyond the public proxy, and an `internal: true` network — your dev compose stack then mirrors the production architecture closely enough that staging issues stop hiding until prod.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# depends_on starts services in order; for real wait-for-ready, add a healthcheck.Try it Yourself »
Discussion
Loading…