Lesson 5 of 6

Docker Compose

By Lesson 4 your app was three long docker run commands with a network and a volume — commands nobody will remember next Tuesday, let alone your teammate cloning the repo fresh. Docker Compose replaces all of that with one YAML file, checked into the repo, that describes the entire stack. Then the whole thing is one command: docker compose up.

💡 The Bottom Line

Compose is "my whole dev stack as one file." Every service, port, volume, and environment variable is declared in compose.yaml, so bringing up the app — on any machine — is docker compose up, and tearing it all down is docker compose down.

The file, end to end

A typical web app + database, exactly equivalent to the manual commands from Lesson 4:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/postgres
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

The daily commands

docker compose up -d       # build (if needed) and start everything
docker compose ps          # status of this project's services
docker compose logs -f api # follow one service's logs
docker compose up -d --build   # force a rebuild after code changes
docker compose down        # stop and remove containers + network
docker compose down -v     # ...and delete the volumes too (data reset)

Compose scopes everything to the project (named after the folder), so down cleans up exactly what up created — no hunting through docker ps for strays.

🙋 There Are No Dumb Questions

Q: Does depends_on wait for the database to be ready?
A: By default it only waits for the container to start — and Postgres takes a moment after starting before it accepts connections. If your API crashes on first boot with "connection refused," that's why. The fix is a healthcheck on the db service plus depends_on: { db: { condition: service_healthy } } — or simply an API that retries its database connection, which you want in production anyway.

⚠️ Watch Out

Editing your source code does not update a service built with build: — the image was baked at the last build. If your changes "aren't showing up," you ran up without --build. During active development, either run docker compose up -d --build after changes or add a bind mount of your source folder so edits appear live.

🔧 Try It Yourself

Save the example above as compose.yaml in the app folder from Lesson 3 (any app that reads DATABASE_URL — or trim the api service down to just the db if you don't have one), then:

docker compose up -d
docker compose ps
docker compose down

Notice what you did not have to do: create a network, create a volume, or remember a single docker run flag.

✅ Quick Check

In the example, the API connects to the database using hostname db. Why does that work?


Last lesson: the mistakes everyone makes once — exiting containers, gigantic images, vanishing data, and the other classic Docker gotchas.

← Back to
Next →