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.
| Feature | Legacy Compose V1 (Deprecated) | Modern Compose V2 (Active Standard) |
|---|---|---|
| CLI Invocation | docker-compose up (Hyphenated external binary) | docker compose up (Native Docker CLI plugin) |
| Canonical Filename | docker-compose.yml | compose.yaml (or compose.yml) |
| Version Schema Header | Mandatory version: '3.8' header | Top-level version key is officially deprecated by the Compose Specification! |
| BuildKit Engine | Optional / required env variables | Enabled natively out-of-the-box |
2. The 4 Top-Level Compose Keys
A compose.yaml document organizes application architecture across four primary root keys:
Container Workloads
Defines the containers, images, ports, and environment variables.
Software Bridges
Configures custom virtual bridge networks for inter-service communication.
Persistent Storage
Declares named volumes managed on the host filesystem for databases.
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 →