Skip to content
Episode 64 · Twenty-three steps to start

Docker Compose: a whole local stack in one file

Monday, 9:00: Elif starts at StayHub, and by Tuesday evening she still can't run the app. The README has 23 setup steps, and step 14 is out of date.

Your app, its database, its cache and its worker, all started with one command from one file.

30 min intermediateDocker ComposeStartup order with healthchecksEnv files, interpolation and overridesWhen Compose stops being enoughService discovery by name on user-defined networks
  1. Problem

    A new engineer joins the booking team. Running the app needs a web server, a background worker, PostgreSQL 17, Redis 7, a migration step and six environment variables. The README has 23 steps and is out of date.

  2. Why the simple system fails

    Long `docker run` commands in a wiki drift, start in the wrong order (the API crashes because Postgres isn't ready), and every laptop ends up slightly different.

  3. Engineering concept

    Describe the whole stack declaratively: services, how they're built, how they connect, what they depend on and how they know they're healthy. One command brings it to that state.

  4. Technology

    compose.yaml services, networks, volumes, healthchecks with depends_on conditions, env files and interpolation, override files, profiles, one-off migration services, watch mode.

  5. Trade-offs

    Fast, reproducible local and single-host environments, but no multi-host scheduling, limited rollout control and no autoscaling. Compose is not a production orchestrator for most teams.

01

The problem

2 min
Tuesday, 18:20. Elif's second day at StayHub.
  1. Maya · Product manager
    Elif started yesterday. She still hasn't seen the booking page run on her laptop.
  2. You · Engineer at StayHub
    The README has 23 steps. Step 14 installs Redis 6; we've used Redis 7 since spring.
  3. Lina · Senior engineer, your mentor
    You lead this one. I'll check your answers.
02

The big idea

4 min
From 23 steps to one command
The big idea

Describe the whole stack in one file. One command brings every laptop to the same state.

A shell script of docker run commands says how, step by step, and breaks if something already runs. Compose describes what should exist; up converges to it from any state.

03

How it works

5 min
What `docker compose up` does
  1. 1
    Reads compose.yaml

    starts a set of containers from one file. Each service (web, db) has an image or build, settings, ports, volumes. docker compose config prints the final result.

  2. 2
    Creates a network and volumes

    A private network, so web reaches db:5432 and cache:6379 by name. pgdata is created once and kept across down and up.

  3. 3
    Starts in dependency order

    depends_on says which services come first. Alone, it only means started: Postgres needs ~4 s to accept connections.

  4. 4
    Waits for ready, not started

    A asks "are you ready?" (pg_isready). With condition: service_healthy, web waits for a ready database.

  5. 5
    Runs one-off steps once

    A migrate service runs migrations and exits; web and worker wait with condition: service_completed_successfully.

  6. 6
    Converges

    Run up again and nothing changes unless the file did; change one service and only it is recreated.

04

Watch a decision being made

4 min
Worked example

You write StayHub's compose.yaml; Lina checks

Replace the 23-step README: docker compose up starts web, worker, PostgreSQL 17, Redis 7 and migrations, in order, with no real secrets in git.

  1. 1List the pieces, wire them by name

    Five services, one volume, six settings. Only web publishes a port: 127.0.0.1:3000:3000.

    Why: Lina: publish only what you open in a browser, or you'll clash with someone's local Postgres.

  2. 2Express readiness

    Health checks on db and cache; web and worker depend on them with service_healthy.

    yaml
    db:
      image: postgres:17
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U app -d stayhub"]
        interval: 3s
        retries: 20
    web:
      depends_on:
        migrate: { condition: service_completed_successfully }

    Why: Removes the half-the-time ECONNREFUSED crash without guessing. Lina: the app should still retry; production doesn't run Compose.

  3. 3Migrate once

    A migrate service starts once db is healthy; everything else waits for it.

    Why: One run, and a failed migration stops the stack with a clear error.

  4. 4Keep secrets out, test like Elif

    Commit .env.example, gitignore .env, pin postgres:17 and redis:7. Fresh clone, up: about 2 minutes to a working booking page.

    Why: Git history is forever; pinned versions stop the Redis 6 bug. Lina: "now the README is four lines."

The whole thing in one line: The README became a file the computer runs: names, health-ordered startup, migrations once, no secrets, the same stack for everyone in two minutes.
05

In real life

3 min
Two ways to give developers a working stack

GitHub: development in the cloud

  • Has publicly described moving development of github.com to Codespaces
  • A huge app; laptop setup took hours
  • A prepared machine in seconds, but needs the internet and a budget

Worth it for a very large codebase with a team to maintain it.

Sentry: the whole product in Compose

  • Publicly documents self-hosting as a Docker Compose setup on one machine
  • Many services start with one script
  • Runs anywhere, offline, for free

Worth it when one machine holds the whole stack.

06

What goes wrong

3 min
Migrations in the web start command

Replicas race; a failed migration crash-loops the app.

Instead

A one-off migrate service, with service_completed_successfully.

`depends_on` without a condition, plus `sleep 10`

Started isn't ready; sleeps are too short or wasteful.

Instead

Health checks and service_healthy; the app retries too.

Committing `.env` with real keys

Secrets stay in git history forever.

Instead

.env.example with dummy values; test-mode keys locally.

07

Trade-offs

3 min
SetupFitsYou pay
Everything nativeSolo projects, no dependenciesVersion drift; long setup docs
Compose for dependencies, app nativeFrontend-heavy apps on macOSApp runtime still drifts
Full stack in ComposeMany services, frequent onboarding, CISync overhead; Docker Desktop memory
Compose in production, one hostInternal toolsSingle point of failure; restart deploys
Friday, 10:00. Karim has seen how simple the file is.
  1. Karim · CTO
    Run production the same way: one 16-core VM, docker compose up -d. Cheaper than three VMs.
  2. Omar · On-call SRE
    One VM is one failure from a full outage. Today, if app-2 dies, app-1 and app-3 keep taking bookings.
  3. Karim · CTO
    VMs rarely die. And we'd save about a third of the server bill.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • What the local stack contains, so a new engineer is productive in minutes, not 2 days.
  • How readiness is expressed: health checks plus conditions, and the app still retries.
  • Whether Compose runs production: not for StayHub's booking site.
Tell Claude Code
Write a compose.yaml for StayHub's local stack: web (our Dockerfile), worker (same image, different command), PostgreSQL 17 with a named volume, Redis 7, a one-off migrate service, Mailpit under a tools profile. db and cache get healthchecks; migrate starts when db is healthy; web and worker start after migrate completes successfully. Publish only web's port, on localhost. The 6 env variables go in a committed .env.example with fake values; real ones in an uncommitted .env. Then run down -v and up ten times and show web started cleanly every time.
Check before you accept
  • Ten cold starts: web starts every time, not "usually".
  • depends_on uses conditions, not just a list of names.
  • Only web has ports:, bound to 127.0.0.1; .env is gitignored.
Red flags
  • It "fixes" the race with restart: always or sleep 10.
  • It publishes the database or Redis ports "for convenience".
  • It suggests production on one large VM with docker compose up.
09

Check yourself

2 min
Q1/3Debugging
About half the time docker compose up ends with web crashing: ECONNREFUSED db:5432. A second up usually works. What's the fix?
services:
  web:
    build: .
    depends_on: [db]
  db:
    image: postgres:17
How confident are you?

Next module: StayHub runs 40 containers on 8 servers. At 3:12 a.m. one server dies, and its containers stay dead until Omar wakes up.

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

Migrations, profiles and secrets

You lead the worked example. Lina's comments are in the "why" lines where she pushed back.

Worked example

You write StayHub's compose.yaml

Replace the 23-step README with one file, so docker compose up starts web, worker, PostgreSQL 17, Redis 7 and the migrations, in the right order, with no real secrets in git.

  1. 1List the pieces

    Five services (web, worker, db, cache, migrate), one volume (pgdata), six settings (database URL, Redis URL, SMTP URL, payment test key, maps key, session secret).

    Why: Every line of the old README maps to one of these. If something isn't on the list, it isn't in the stack.

  2. 2Wire them by name

    web and worker use db:5432 and cache:6379. Only web publishes a port, on loopback: 127.0.0.1:3000:3000.

    Why: Compose creates a private network, so services find each other by name. Lina: "publish only what you open in a browser, or you'll clash with someone's local Postgres."

  3. 3Express readiness

    Health checks on db (pg_isready) and cache (redis-cli ping); web and worker depend on them with condition: service_healthy.

    yaml
    db:
      image: postgres:17
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U app -d stayhub"]
        interval: 3s
        retries: 20

    Why: Started isn't ready. This removes the half-the-time ECONNREFUSED crash without guessing a sleep time.

  4. 4Migrate once

    A migrate service runs npm run db:migrate and exits. web and worker depend on it with condition: service_completed_successfully.

    Why: Only one thing migrates, and a failed migration stops the stack with a clear error instead of a crash loop.

  5. 5Keep secrets out

    Commit .env.example with names and dummy values; gitignore .env; developers use payment test keys. Pin postgres:17 and redis:7 to match production.

    Why: Anything in git stays in its history forever. Pinned versions stop the "Redis 6 on one laptop" bug from coming back.

  6. 6Test it like Elif

    Clone the repo into a fresh folder, cp .env.example .env, docker compose up. Time it: about 2 minutes to a working booking page. Then docker compose down && docker compose up and check the test bookings are still there.

    Why: The only real test of onboarding is a clean start. Lina: "good, and now the README is four lines."

The whole thing in one line: You turned the README into a file the computer runs: services wired by name, startup ordered by health, migrations run once, secrets kept out of git, so a new engineer gets the same stack as everyone else in about two minutes.
bash
cp .env.example .env              # once; fill in personal test keys
docker compose up -d               # start the stack in the background
docker compose watch               # sync code changes into web as you edit
docker compose logs -f web worker  # follow two services
docker compose exec db psql -U app stayhub
docker compose run --rm web npm test     # one-off command in a fresh container
docker compose --profile tools up -d     # include Mailpit
docker compose config              # print the fully resolved file
docker compose down                # remove containers + network, keep volumes
docker compose down -v             # ...and delete volumes: a fresh database
Day-to-day commands.
MechanismWhat it's forGotcha
.env next to compose.yamlInterpolating ${VAR} inside compose.yamlNot automatically passed into containers
env_file:Loading variables into a container's environmentValues are visible via docker inspect; fine for local test keys, not production secrets
environment:Explicit per-service values; overrides env_fileDon't commit real secrets here
compose.override.yamlAuto-merged personal or local tweaksGitignore it if it's personal, or everyone inherits your ports
-f compose.yaml -f compose.ci.yamlExplicit layering for CI or other environmentsLater files override earlier ones key by key
Variable precedence is a frequent source of 'but I set it!' confusion. `docker compose config` shows the final result.
11

Interactive simulation

12

Visual diagram

Click a component

The booking app's local stack as Compose sees it. Click components or start the walkthrough.

13

Analogy

  • Opening a restaurantTelling each employee their job every morningDocker ComposeTyping a docker run per container
  • Opening a restaurantThe posted opening checklistDocker ComposeThe compose.yaml file
  • Opening a restaurantWaiters serve once the kitchen says readyDocker ComposeWaiting for health checks
  • Opening a restaurantStaff calling "kitchen!" by nameDocker ComposeServices reaching each other by name
  • Opening a restaurantA regional manager for fifty branchesDocker ComposeAn orchestrator like Kubernetes

Where the analogy breaks: a checklist can't restart a chef who faints. Compose can restart a crashed container, but if the whole machine goes down, everything goes down with it.

14

Check your understanding

Q1/3Debugging
Which statement is true?

.env contains POSTGRES_VERSION=16 and STRIPE_KEY=sk_test_123. compose.yaml has image: postgres:${POSTGRES_VERSION:-17} for db, and web has no env_file or environment entry for STRIPE_KEY.

How confident are you?
15

Practice ladder

Step 1 · Trace

Trace a cold `docker compose up`

Scenario Warm-up

With your compose.yaml (db and cache have health checks; migrate depends on db healthy; web and worker depend on migrate completing successfully and cache healthy), Elif runs docker compose up on a fresh laptop.

  1. List the order in which things are created and started.
  2. Postgres takes 4 s to become healthy and the migration takes 6 s. Roughly when does web start?
  3. Then she runs docker compose down and docker compose up again. Are her test bookings still there?
Step 2 · Complete

Finish the worker service

Code Practice

Lina leaves the worker service for you. It uses the same image as web, runs npm run worker, needs the database and Redis, must not start before migrations are done, and sends emails through Mailpit (smtp://mailpit:1025), which is in the tools profile.

  worker:
    build: .
    command: ___
    environment:
      DATABASE_URL: ___
      REDIS_URL: ___
      SMTP_URL: smtp://mailpit:1025
    depends_on:
      ___

Fill the gaps. Then answer: should worker publish a port?

Step 3 · Solve

Fix the search team's compose.yaml

Debugging Practice

New engineers on the search team say their stack "works sometimes". Find at least five problems and rewrite the file.

services:
  search-api:
    build: .
    ports: ["3001:3001"]
    environment:
      DATABASE_URL: postgres://search:s3cr3t-prod-pass@localhost:5432/search
      OPENSEARCH_URL: http://opensearch:9200
    depends_on: [db, opensearch]
    command: sh -c "sleep 15 && npm run migrate && npm start"
  db:
    image: postgres:latest
    ports: ["5432:5432"]
    environment: { POSTGRES_PASSWORD: s3cr3t-prod-pass }
  opensearch:
    image: opensearchproject/opensearch:2
    ports: ["9200:9200"]
Step 4 · Decide

Compose in production for the owner portal?

Decision Stretch

The owner portal (900 hotel owners, used during office hours) still runs on owners-1 with plain docker run commands. Karim proposes moving it to docker compose up -d on that same VM. The facts:

  • Owners tolerate an hour of downtime on a rare bad day; guests never use the portal.
  • Deploys happen twice a week and take about 20 s of restart.
  • The database is now managed Postgres; photos are in object storage.
  • The main booking site will move to a proper orchestration platform next quarter.

Write a short decision: "We chose X over Y because…, accepting the risk of…".

16

Explain it yourself

A new teammate asks why the project's compose.yaml has healthchecks, a migrate service and profiles, and why production doesn't just run docker compose up on a server. Explain.

0 words