The last article ran two containers by hand: create a network, create a volume, run the database, run the app, remember the right flags every single time. That works, but it does not scale past a couple of containers, and it is impossible to hand to a teammate as anything other than a wall of commands to copy carefully. Docker Compose exists to fix exactly that.
One file, one application
A compose.yaml file describes every container your application needs—called services—along with the networks and volumes connecting them, in one declarative file:
services: web: build: . ports: - "3000:3000" environment: DATABASE_URL: postgres://postgres:devpassword@db:5432/postgres depends_on: - db db: image: postgres:16 environment: POSTGRES_PASSWORD: devpassword volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata:
That one file replaces every command from the last article's stateful example: the network is created automatically, web can reach db by name without any extra configuration, and pgdata persists exactly the way the named volume did before.
The commands you actually use
docker compose up # build/start everything, attached docker compose up -d # same, but detached docker compose down # stop and remove containers and the default network docker compose logs -f web # follow one service's logs docker compose ps # what's running, per service
docker compose up reads compose.yaml in the current directory, creates a network for the whole project automatically, builds any service with a build: key, pulls any service with an image: key, and starts everything in the right order based on depends_on.
depends_on solves ordering, not readiness
depends_on: [db] guarantees Compose starts the db container before web. It does not guarantee Postgres has finished initializing and is ready to accept connections by the time web starts—a container can be "started" long before the process inside it is actually ready to do useful work. For real projects, pair depends_on with a retry loop in the application's own startup code, or a healthcheck: on the db service and a depends_on: { db: { condition: service_healthy } } on web. A beginner setup can usually get away without this; a real one should not skip it.
Why this is the natural next step from docker run
Everything in compose.yaml maps directly to flags you already know from docker run:
| Compose key | Equivalent docker run flag |
|---|---|
image: / build: | the image argument |
ports: | -p |
environment: | -e |
volumes: | -v |
depends_on: | (manual ordering, done by hand before) |
Nothing here is a new concept. Compose is a way to write down the commands you were already typing, so the whole application starts the same way every time, for every person on the team, without anyone needing to remember the right combination of flags.
Common mistakes
- Assuming
depends_onmeans "ready," not just "started." A database container can be running long before the database inside it accepts connections. - Putting real secrets directly in
compose.yaml. Use an.envfile (Compose reads one automatically) orenvironment: - MY_VARreferencing a value set outside the file, and keepcompose.yamlitself safe to commit. - Forgetting
docker compose downremoves containers, not named volumes. Data in a named volume survivesdown; add-vif you actually intend to delete it too, and be sure that is what you want. - Rebuilding by hand instead of using Compose.
docker compose up --buildrebuilds images defined withbuild:before starting—no need to rundocker buildseparately.
Practical checklist
- My project's services, networks, and volumes are defined in one
compose.yaml, not a list of manual commands. - I can start, stop, and tail logs for the whole application with
docker compose up,down, andlogs -f. - I understand that
depends_oncontrols start order, not readiness. - Real secrets are supplied through an
.envfile or the environment, not hardcoded intocompose.yaml. - I know that
docker compose downkeeps named volumes unless I explicitly add-v.
Next in the series: Docker vs. Podman: What Actually Changes. We will put the second engine this series promised side by side with everything you have learned so far.


