Adetayo Akinsanya unkletayo.dev

Multi-Container Orchestration: Docker Compose Architecture and Declarative Configs

Declarative application definitions, service discovery networks, and depends_on health check sequencing.

Part 17 in Series — Catch up on the previous article: Docker Storage Options: Named Volumes, Bind Mounts, and tmpfs Mount Mechanics (Part 16) before diving into this post.

Onboarding a new software engineer at a fintech startup used to require a 6-page setup wiki.

The engineer had to execute 12 complex docker run commands in exact sequential order, manually creating bridge networks, mounting host directories, allocating ports, and passing 40 environment variables:

docker network create app-net
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=secret postgres:15
docker run -d --name redis --network app-net redis:7-alpine
docker run -d --name worker --network app-net -e DB_HOST=db -e REDIS_HOST=redis my-worker
docker run -d --name api --network app-net -p 8080:8080 -e DB_HOST=db my-api

If the engineer ran the my-api container before the db container finished initializing its database socket, my-api crashed immediately.

When the engineering team introduced Docker Compose:

docker compose up -d

The entire multi-container environment—including databases, caches, message queues, virtual networks, and volume mounts—initialized cleanly in 15 seconds.

How does Docker Compose translate a single declarative YAML file into an orchestrated multi-container architecture?


1. What Docker Compose Is (and Is Not)

Docker Compose is a tool for defining and running multi-container applications on a single host.

IMPERATIVE SHELL SCRIPTS                     DECLARATIVE COMPOSE SPEC (YAML)
+------------------------------------+       +------------------------------------+
| docker run -d --name db ...        |  vs   | services:                          |
| docker run -d --name redis ...     |       |   db: { image: postgres:15 }       |
| (Prone to typos & order errors)    |       |   web: { build: . , ports: ... }   |
+------------------------------------+       +------------------------------------+
  • Imperative Approach (docker run): You manually specify step-by-step commands instructing the engine how to construct resources.
  • Declarative Approach (docker-compose.yml): You state what desired state the application environment should possess. Docker Compose inspects the host, compares current state to desired state, and executes necessary changes.

2. Anatomy of a docker-compose.yml File

A Compose specification organizes multi-container applications across three main top-level keys: services, networks, and volumes.

version: '3.8'

services:
  # Service 1: PostgreSQL Database
  database:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: app_production
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend-net

  # Service 2: Node.js API Web Server
  web-api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      DB_HOST: database
    depends_on:
      database:
        condition: service_healthy  # Waits for health check readiness!
    networks:
      - backend-net

networks:
  backend-net:
    driver: bridge

volumes:
  db-data:

3. The depends_on Trap vs Health Check Readiness

A common misunderstanding in Docker Compose involves the depends_on property.

The Problem with Simple depends_on

# INSUFFICIENT:
depends_on:
  - database

If you use simple depends_on, Compose guarantees only that the database container process is started before launching web-api.

However, starting a PostgreSQL container process takes 5 milliseconds, while initializing database system tables on disk takes 3 seconds. web-api launches, attempts to connect to port 5432 before PostgreSQL is ready to accept connections, and crashes!


The Solution: depends_on + condition: service_healthy

To enforce true readiness sequencing, pair depends_on with a Container Health Check:

  web-api:
    depends_on:
      database:
        condition: service_healthy # Pauses web-api until healthcheck passes!
  1. Compose launches the database container.
  2. Compose executes pg_isready inside database every 5 seconds.
  3. Once PostgreSQL finishes database initialization and pg_isready returns exit code 0 (healthy), Compose launches web-api.

4. Execution Lifecycle: What docker compose up Does

When you run docker compose up -d, the Compose engine executes an automated 6-step lifecycle:

1. Parse docker-compose.yml & validate syntax
        |
        v
2. Create User-Defined Network (e.g., app_backend-net)
        |
        v
3. Create & Allocate Named Volumes (e.g., app_db-data)
        |
        v
4. Build missing container images (if 'build' is specified)
        |
        v
5. Evaluate dependency tree & launch containers in topological order
        |
        v
6. Monitor container health & stream logs

Imperative docker run vs Declarative docker compose

FeatureImperative docker runDeclarative docker compose
Configuration FormatLong CLI shell scriptsSingle version-controlled docker-compose.yml
Service DiscoveryManual network creation requiredAutomatic isolated network per compose file
Startup OrderManual script executionTopological dependency resolution (depends_on)
Environment LifecycleMust manage containers individuallyManage entire stack as one unit (up / down)
State Drift ResolutionRe-running script causes container name conflictsRecreates only modified services automatically

Summary & Next Steps

Docker Compose simplifies multi-container orchestration for development and single-host deployment:

  • Declarative YAML Specifications (docker-compose.yml) define services, networks, and volumes as version-controlled code.
  • Automatic User-Defined Networks allow containers within the same Compose stack to resolve each other by service name.
  • depends_on + condition: service_healthy ensures containers wait for dependency readiness rather than just process creation.
  • docker compose up automates network creation, volume allocation, and container startup in topological order.

In the next article, we inspect Container Hardening: Non-Root Execution, Linux Capabilities, and Read-Only Filesystems.

References & Further Reading

  1. Center for Internet Security (CIS). (2023). CIS Docker Benchmark v1.6.0 Guidelines. CIS Security.
  2. Rootless Containers Project. Rootless Containers Architecture and Unprivileged User Namespaces. Rootlesscontaine.rs Docs.
  3. Docker Inc. Run the Docker Daemon as a Non-Root User (Rootless Mode). Docker Docs.

Up Next in Series →

Part 18: Container Hardening: Non-Root Execution, Linux Capabilities, and Read-Only Filesystems

Continue to Part 18 →