Almost nothing real is a single container — you've got an app, a database, maybe Redis. docker-compose.yml declares all of them, plus the network and volumes tying them together, in one file:
services:
web:
build: .
ports: ["3000:3000"]
environment:
- DATABASE_URL=postgres://db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=devpassword
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
docker compose up builds and starts everything declared, wired together on a shared user-defined network where web can reach db by service name alone — this is the "automatic name resolution" mentioned in the networking section, provided for free by Compose. docker compose down tears everything back down; add -v to also remove the declared volumes (data loss, deliberately opt-in).
depends_on controls start order, not readiness — by default, Compose starts db before web but doesn't wait for Postgres to actually be ready to accept connections, just for its container process to have started. condition: service_healthy (paired with a healthcheck, above) is what actually makes Compose wait for the dependency to be ready, not merely started — a very common source of "works when I restart it, fails on first up" bugs is relying on plain depends_on for a database that takes a few seconds to initialize before accepting connections.