Frontend Labs
⋯

Chapter 51 — Docker Compose: running more than one container together

In one sentence

Docker Compose lets you describe an entire multi-container application — every service, its image, its ports, its volumes, its dependencies on other services — in ONE declarative YAML file, and bring the whole thing up or down with a single command.

Why this matters

Almost no real application is a single container — Compose is how you develop and test a realistic, multi-service setup locally, and it's the direct conceptual predecessor to Kubernetes' declarative model (Part 16).

How it actually works

   # docker-compose.yml
   services:
     web:
       build: .                       # build FROM a local Dockerfile
       ports:
         - "8080:80"                   # Chapter 50's -p, declaratively
       depends_on:
         - db                           # start `db` first (doesn't wait
                                       # for it to be actually READY --
                                       # a common, real gotcha)
       environment:
         DATABASE_URL: "postgresql://db:5432/myapp"  # Chapter 50's
                                       # container-name DNS resolution,
                                       # used directly

     db:
       image: postgres:16               # or pull an existing image
       volumes:
         - dbdata:/var/lib/postgresql/data   # Chapter 49's volumes,
                                       # declaratively
       environment:
         POSTGRES_PASSWORD: "devpassword"

   volumes:
     dbdata:                            # declare the named volume
   docker compose up                     # build/pull images, create
                                        # the network, start everything,
                                        # in the RIGHT ORDER
   docker compose up -d                    # detached (background)
   docker compose down                      # stop and remove
                                        # everything (containers,
                                        # network -- volumes SURVIVE by
                                        # default, exactly Chapter 49's
                                        # deliberate separation)
   docker compose logs -f web                # follow ONE service's
                                        # logs
   docker compose ps                          # what's running, in
                                        # this project

Real-world example

   `depends_on`'s actual behaviour is one of the most common, well-
   documented real points of confusion for engineers new to Compose:
   Docker Compose's own official documentation is explicit that, by
   default, `depends_on` only controls STARTUP ORDER -- it starts `db`
   BEFORE `web`, but it does NOT wait for the database to actually be
   READY to accept connections (a database process can take several
   real seconds to initialise after its CONTAINER has technically
   started, Chapter 3's process-vs-"actually ready" distinction). A
   very common real symptom: `web`'s application code tries to connect
   to `db` immediately on startup, the database isn't accepting
   connections yet, and the application crashes or errors out --
   despite `depends_on` being correctly configured, exactly as
   documented.
   THE REAL, DOCUMENTED FIX Compose's own docs recommend: either build
   RETRY LOGIC into your application's own startup/connection code
   (the generally-preferred, more robust real-world approach, since it
   also protects you in production, not just local dev), or use
   `depends_on`'s `condition: service_healthy` option paired with an
   explicit HEALTHCHECK definition on the dependency service, so
   Compose actually waits for a real readiness signal, not just "the
   container process started." This exact "started" vs. "actually
   ready" distinction reappears, in a more powerful form, as
   Kubernetes' readiness probes (Chapter 60) -- worth remembering this
   Compose gotcha as the direct conceptual ancestor of that later,
   more robust mechanism.

Try it yourself

mkdir -p ~/lab/composedemo && cd ~/lab/composedemo
cat > docker-compose.yml << 'EOF'
services:
  web:
    image: alpine
    command: sh -c "echo waiting; sleep 3; ping -c 2 db"
    depends_on:
      - db
  db:
    image: alpine
    command: sh -c "sleep 300"
EOF

docker compose up
docker compose ps
docker compose logs web
docker compose down

Common mistakes

  • Trusting depends_on alone for "the service I depend on is actually ready." It only guarantees START ORDER — build real readiness checks (retries, healthchecks) into your application.
  • Forgetting docker compose down preserves volumes by default. Good, deliberate safety (Chapter 49) — but surprising the first time if you expected a truly clean slate; add -v explicitly if you genuinely want volumes removed too.
  • Hardcoding secrets directly in docker-compose.yml. Use an .env file (excluded from version control) or a proper secrets-management approach instead — a committed compose file with real credentials in it is a common, real leak.

Check yourself

  1. What does depends_on actually guarantee, and what does it NOT guarantee?
  2. What happens to volumes when you run docker compose down (without -v)?
  3. What's the more robust, production-relevant fix for the "started vs. ready" problem, and what Kubernetes mechanism does it foreshadow?

Further reading

  • Docs: Docker's own official Compose specification and "Control startup and shutdown order" documentation.
  • Reference: docker compose --help.
  • Practice: convert a small, real two-service idea (a web app + a database) into your own docker-compose.yml from scratch.

Generated from real-life-os-guide.md · 8 Oct 2026 16:36