iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…