Chapter 11 • Multi-Container & Compose

Docker Compose Basics & Specification

Modern applications are rarely single monoliths. They consist of databases, cache layers, API servers, and reverse proxies. Docker Compose enables developers to define and orchestrate multi-container application stacks declaratively in a single compose.yaml file.

1. Compose V2 Evolution: docker compose

In legacy systems, Compose was an external Python utility invoked with a hyphen (docker-compose). Today, Compose V2 is rewritten in Go and integrated directly into the Docker CLI engine as docker compose.

FeatureLegacy Compose V1 (Deprecated)Modern Compose V2 (Active Standard)
CLI Invocationdocker-compose up (Hyphenated external binary)docker compose up (Native Docker CLI plugin)
Canonical Filenamedocker-compose.ymlcompose.yaml (or compose.yml)
Version Schema HeaderMandatory version: '3.8' headerTop-level version key is officially deprecated by the Compose Specification!
BuildKit EngineOptional / required env variablesEnabled natively out-of-the-box

2. The 4 Top-Level Compose Keys

A compose.yaml document organizes application architecture across four primary root keys:

services:
Container Workloads

Defines the containers, images, ports, and environment variables.

networks:
Software Bridges

Configures custom virtual bridge networks for inter-service communication.

volumes:
Persistent Storage

Declares named volumes managed on the host filesystem for databases.

secrets / configs:
Security & Files

Mounts sensitive credentials and static configs without leaking into images.

3. Solving the Database Startup Race Condition

A common bug in multi-container setups is the API container starting before the database is ready. Standard depends_on: [db] only waits for the database container to start, not for PostgreSQL to finish initializing its internal transaction logs and listen on port 5432!

Modern Compose solves this using Health-Aware Dependencies (condition: service_healthy):

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: "postgresql://app_user:secret@postgres-db:5432/production_db"
    # Wait until PostgreSQL healthcheck passes before booting the API!
    depends_on:
      postgres-db:
        condition: service_healthy

  postgres-db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: production_db
    volumes:
      - pg_data:/var/lib/postgresql/data
    # Native Healthcheck probe:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d production_db"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

volumes:
  pg_data:

4. Ports vs Expose: Crucial Security Nuance

ports: ["5432:5432"] (Published Externally)

Maps the port to the host machine's network interfaces. Anyone who knows your host server's public IP can connect to this port unless blocked by host firewall rules. Never expose internal databases on 0.0.0.0 in production!

expose: ["5432"] (Internal Only)

Makes the port accessible only to sibling containers attached to the same Compose network. The port is completely invisible and unreachable from the external host or public internet!

Next Up: Multi-Container Application Stacks

In Chapter 12, construct a complete enterprise 3-tier architecture: Nginx reverse proxy + Node.js/Go API + PostgreSQL + Redis caching.

Chapter 12: Multi-Container Stack →

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools