The Symphony of Containers

[!NOTE] This module explores the core principles of The Symphony of Containers, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.

1. The Problem: “Run Command Hell”

Imagine you are onboarding a new developer. You send them this slack message:

“Hey, to run the app, first pull mongo:5. Then run it on port 27017. Then build the backend image. Run that, but make sure you link it to the mongo container. Oh, and don’t forget to set MONGO_URI env var. Then build the frontend…”

This is imperative orchestration. You are describing how to do it, step-by-step. It is error-prone, hard to share, and impossible to version control.

2. The Solution: Declarative Orchestration

Docker Compose allows you to define the desired state of your application in a single file: docker-compose.yaml.

Instead of running commands, you write a “recipe” for your infrastructure.

[!TIP] Compose vs Dockerfile

  • Dockerfile: Describes how to build a single image (the recipe for a cake).
  • Compose: Describes how to run multiple containers together (the menu for the dinner party).

3. Anatomy of docker-compose.yaml

A Compose file has three main sections:

  1. Services: The computing components (your app, database, cache).
  2. Networks: The communication channels.
  3. Volumes: The persistent storage.

Interactive: The Compose Architect

Click the buttons to add services to your stack and watch the YAML generate automatically.

Add Service

(Your stack is empty)
services:

4. The Versioning Confusion

You might see version: '3.8' or version: '2' in older files.

[!NOTE] The Modern Standard Since the Docker Compose Specification (2020+), the top-level version field is optional and essentially deprecated. Modern Compose (docker compose, note the lack of hyphen) simply ignores it and treats the file according to the latest spec.

We recommend omitting the version field for new projects.

5. A Real-World Example

Here is a typical stack: A Go backend, a React frontend, and a Postgres database.

services:
  # 1. Frontend Service
  frontend:
    build:
      context: ./client
      dockerfile: Dockerfile
    ports:
      - "80:80"      # Host:Container
    depends_on:
      - backend      # Start backend first

  # 2. Backend Service
  backend:
    build: ./server  # Shorthand for context
    environment:
      - DB_HOST=database
      - DB_USER=postgres
    ports:
      - "8080:8080"
    depends_on:
      database:
        condition: service_healthy

  # 3. Database Service
  database:
    image: postgres:15-alpine
    environment:
      - POSTGRES_PASSWORD=secret
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  db_data:           # Named volume for persistence

Key Fields Explained

  1. build vs image:
    • build: Creates an image from a local Dockerfile.
    • image: Pulls a pre-built image from a registry (Docker Hub).
  2. ports: Maps Host_Port:Container_Port.
  3. volumes: Mounts storage. db_data is a named volume managed by Docker (survives container restart).
  4. depends_on: Controls startup order.
    • Basic: “Start DB before Backend”.
    • Advanced: “Wait until DB is actually healthy (responding to pings) before starting Backend”.

6. Why This Matters

By committing this file to Git, you guarantee that every developer on your team runs the exact same environment.

  • No “I forgot to install Postgres”.
  • No “Which version of Redis are we using?”.
  • No “It works on my machine”.

If it works in Compose, it works everywhere.