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 setMONGO_URIenv 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:
- Services: The computing components (your app, database, cache).
- Networks: The communication channels.
- 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
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
versionfield 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
buildvsimage:build: Creates an image from a localDockerfile.image: Pulls a pre-built image from a registry (Docker Hub).
ports: MapsHost_Port:Container_Port.volumes: Mounts storage.db_datais a named volume managed by Docker (survives container restart).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.