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:
services— one entry per container.apiis built from the local Dockerfile (build: .);dbuses a published image.- Networking is automatic — Compose puts all services on a shared network, with each service reachable by its name. That's why the connection string says host
db. depends_on— start order: bringdbup beforeapi.volumes— named volumes declared at the bottom, mounted per service.
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.