Docker Compose in Depth

Multi-container orchestration, configuration, and best practices
Compose Services YAML Orchestration
Brendan Lynskey · 2026
01

What is Docker Compose?

A tool for defining and running multi-container Docker applications using a single YAML configuration file.

Without Compose

  • Manual docker run per container
  • Separate network/volume creation
  • Complex startup ordering
  • Hard to reproduce environments

With Compose

  • Single docker compose up command
  • Declarative YAML configuration
  • Automatic networking between services
  • Reproducible across environments
# One command to rule them all
docker compose up -d
02

Compose File Structure

A compose.yaml file has three top-level keys: services, networks, and volumes.

services:          # Container definitions
  web:
    image: nginx:alpine
    ports: ["80:80"]
    networks: [frontend]
    volumes: [static:/usr/share/nginx/html]

  api:
    build: ./api
    networks: [frontend, backend]

  db:
    image: postgres:16
    volumes: [pgdata:/var/lib/postgresql/data]
    networks: [backend]

networks:          # Custom networks
  frontend:
  backend:
    driver: bridge

volumes:           # Named volumes
  pgdata:
  static:
03

Service Configuration Deep Dive

Each service supports dozens of configuration keys. Here are the most important ones.

Container Basics

  • image — base image
  • build — build context
  • container_name
  • command / entrypoint
  • working_dir

Networking

  • ports — host:container
  • expose — internal only
  • networks
  • hostname
  • dns

Runtime

  • environment / env_file
  • volumes / tmpfs
  • restart policy
  • deploy.resources
  • healthcheck
services:
  app:
    image: node:20-alpine
    container_name: my-app
    working_dir: /app
    command: ["node", "server.js"]
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: "1.0", memory: 512M }
04

Build vs Image

Choose between pulling a pre-built image or building from a Dockerfile.

Using image:

services:
  db:
    image: postgres:16-alpine
    # Pulls from Docker Hub
    # or a private registry
  • Fast startup — no build step
  • Ideal for databases, proxies, caches

Using build:

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile.prod
      args:
        NODE_ENV: production
      target: runtime
      cache_from:
        - myregistry/api:cache
  • Custom application images
  • Supports multi-stage targets

Combine both: set build and image together to tag the built image for pushing to a registry.

05

Environment Variables & .env Files

Three ways to inject environment variables into services.

Inline

environment:
  - DB_HOST=db
  - DB_PORT=5432
  - DEBUG=false

env_file

env_file:
  - .env
  - .env.local

Loaded in order; later files override earlier ones.

Shell / .env

# compose.yaml
services:
  db:
    image: postgres:${PG_VER}
# .env file
PG_VER=16-alpine
SourceScopePriority (highest first)
CLI -eSingle run1 (highest)
Shell environmentHost process2
environment: keyCompose file3
env_file:External file4
.env fileVariable substitution5 (lowest)
06

depends_on & Healthchecks

Control startup order and wait for services to be truly ready, not just started.

Basic depends_on

services:
  api:
    depends_on:
      - db
      - redis
  # Starts db and redis first,
  # but does NOT wait for them
  # to be "ready"

With Healthcheck Condition

services:
  api:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
db starts healthcheck passes api starts
07

Networking in Compose

Compose creates a default bridge network for your project. Services resolve each other by service name.

Default Behaviour

  • Auto-creates <project>_default network
  • All services join it automatically
  • DNS resolution by service name
  • api can reach db:5432

Custom Networks

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # no internet

services:
  proxy:
    networks: [frontend]
  api:
    networks: [frontend, backend]
  db:
    networks: [backend]
FeatureDefault NetworkCustom Network
IsolationAll services see each otherFine-grained segmentation
Internet accessYesConfigurable via internal
Subnet controlAuto-assignedipam config supported
08

Volume Management in Compose

Persist data beyond container lifecycle using named volumes, bind mounts, or tmpfs.

Named Volume

services:
  db:
    volumes:
      - pgdata:/var/lib/
        postgresql/data

volumes:
  pgdata:

Managed by Docker. Survives compose down.

Bind Mount

services:
  app:
    volumes:
      - ./src:/app/src
      - ./config:/app/config:ro

Maps host path directly. Great for development.

tmpfs

services:
  app:
    tmpfs:
      - /tmp
      - /run:size=64M

In-memory filesystem. Lost on restart.

Warning: docker compose down -v deletes named volumes. Use with caution in production.

09

Compose Profiles

Selectively start services based on profiles — perfect for optional tools, debug containers, or test infrastructure.

services:
  web:
    image: nginx:alpine       # Always starts (no profile)

  api:
    build: ./api              # Always starts (no profile)

  debug:
    image: busybox
    profiles: [debug]         # Only with --profile debug

  test-db:
    image: postgres:16
    profiles: [testing]       # Only with --profile testing

  mailhog:
    image: mailhog/mailhog
    profiles: [debug, testing] # Starts with either profile

Start specific profiles

docker compose --profile debug up -d
docker compose --profile testing up -d

Multiple profiles

docker compose --profile debug \
  --profile testing up -d
# Or use COMPOSE_PROFILES env var
COMPOSE_PROFILES=debug,testing
10

Compose Watch (Hot Reload)

Automatically sync, rebuild, or restart services when source files change.

services:
  web:
    build: ./web
    develop:
      watch:
        - action: sync           # Copy changed files into container
          path: ./web/src
          target: /app/src
          ignore:
            - node_modules/

        - action: rebuild        # Full rebuild on Dockerfile changes
          path: ./web/Dockerfile

        - action: sync+restart   # Sync then restart the service
          path: ./web/config
          target: /app/config

sync

Hot-swap files without restart. Best for interpreted languages.

rebuild

Triggers full image rebuild. Use for dependency or Dockerfile changes.

sync+restart

Copy files then restart container. For config changes that need a reload.

docker compose watch    # Start watching for changes
11

Multi-Environment Configs

Use override files to layer environment-specific configuration on top of a base.

compose.yaml (base)

services:
  api:
    build: ./api
    environment:
      - NODE_ENV=production
    restart: always

compose.override.yaml (dev)

services:
  api:
    environment:
      - NODE_ENV=development
      - DEBUG=true
    volumes:
      - ./api/src:/app/src
    ports:
      - "3000:3000"
    restart: "no"

Compose automatically merges compose.yaml + compose.override.yaml.

# Dev (auto-loads override)
docker compose up -d

# Production (explicit files, skips override)
docker compose -f compose.yaml -f compose.prod.yaml up -d

# Staging
docker compose -f compose.yaml -f compose.staging.yaml up -d
12

Scaling Services

Run multiple instances of a service for load balancing and high availability.

CLI Scaling

# Scale to 3 instances
docker compose up -d --scale api=3

# Scale multiple services
docker compose up -d \
  --scale api=3 \
  --scale worker=5

Cannot use container_name or fixed ports with scaling.

Deploy Replicas

services:
  api:
    build: ./api
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure
    # Use port ranges
    ports:
      - "3000-3002:3000"
# Load-balance with Nginx upstream
services:
  proxy:
    image: nginx:alpine
    ports: ["80:80"]
    depends_on: [api]
  api:
    build: ./api
    deploy:
      replicas: 3
    expose: ["3000"]     # Internal only, proxy routes traffic
13

Compose Commands Cheat Sheet

CommandDescription
docker compose up -dStart all services in detached mode
docker compose downStop and remove containers, networks
docker compose down -vAlso remove named volumes
docker compose buildBuild or rebuild service images
docker compose pullPull latest images for services
docker compose logs -f apiFollow logs for a specific service
docker compose psList running containers
docker compose exec api shShell into a running container
docker compose run api npm testRun a one-off command
docker compose configValidate and view merged config
docker compose watchStart file-watching for hot reload
docker compose topDisplay running processes
docker compose cpCopy files to/from containers
14

Production vs Development Configs

ConcernDevelopmentProduction
BuildUse build: with watchUse image: from registry
VolumesBind mounts for live editingNamed volumes only
PortsExpose debug portsExpose only necessary ports
Restartrestart: "no"restart: always
LoggingDebug/verbose levelStructured JSON logging
ResourcesUnconstrainedCPU/memory limits set
SecretsInline env varssecrets: or external vault

Development Tips

  • Use compose watch for hot reload
  • Include debug tools (mailhog, pgadmin)
  • Mount source code as bind mounts

Production Tips

  • Pin image tags — never use :latest
  • Set resource limits on every service
  • Use healthchecks and restart policies
15

Docker Compose vs Kubernetes

Both orchestrate containers, but at very different scales and complexity levels.

AspectDocker ComposeKubernetes
ScopeSingle hostMulti-node cluster
ConfigOne YAML fileMultiple manifests (Deployments, Services, Ingress...)
ScalingManual replicasAuto-scaling (HPA, VPA)
NetworkingBridge networksOverlay, Service mesh, Ingress
Self-healingRestart policiesFull reconciliation loop
SecretsEnvironment / filesNative Secrets, external vaults
Learning curveLowHigh
Best forDev, CI/CD, small prodLarge-scale production

Tip: Use Compose for development and CI, then graduate to Kubernetes when you need multi-node orchestration. Tools like kompose help convert Compose files to K8s manifests.

16

Compose Extensions & YAML Anchors

Eliminate duplication using YAML anchors and Compose extension fields.

# Extension field (x-) ignored by Compose, used for anchors
x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options: { max-size: "10m", max-file: "3" }

x-env: &default-env
  TZ: UTC
  LOG_LEVEL: info

services:
  api:
    <<: *common                 # Merge anchor
    build: ./api
    environment:
      <<: *default-env
      PORT: "3000"

  worker:
    <<: *common                 # Reuse same config
    build: ./worker
    environment:
      <<: *default-env
      QUEUE: jobs

& (Anchor)

Defines a reusable YAML block. Placed on the source definition.

* (Alias) + << (Merge)

References the anchor. <<: merges keys into the current mapping.

17

Summary & Further Reading

Key Takeaways

  • Compose simplifies multi-container apps into one YAML file
  • Use healthchecks with depends_on for proper ordering
  • Profiles let you toggle optional services
  • Compose Watch enables hot reload in development
  • Override files separate dev/staging/prod configs
  • YAML anchors eliminate configuration duplication
  • Compose is ideal for dev and small prod; Kubernetes for scale
Compose Services YAML Orchestration

Brendan Lynskey · 2026