Lesson 04 of 13 · Level 2 — Docker Compose
Docker Compose basics
Describe a whole multi-container app in one file and start it with one command. Learn the compose.yaml structure, how services find each other, environment variables and .env files, and the day-to-day docker compose commands.
Why Compose
Running a three-service app with plain docker run means three long commands, a network, a volume, and remembering the order. Docker Compose puts all of it in one declarative file you can keep in Git:
$ docker compose up -d # create and start everything
$ docker compose down # stop and remove it again
docker run is ordering each dish separately and telling the waiter which table, in which order, every single time. A Compose file is the set menu written on a card: hand it over once and the whole meal arrives, in the right order, at the right table.
docker compose, not docker-compose
The original docker-compose (v1, written in Python) is no longer maintained. Today Compose is a Docker CLI plugin, run as docker compose (with a space). It's installed with Docker Engine from Docker's packages. The top-level version: key in old files is obsolete; current Compose ignores it and prints a warning.
A first compose.yaml
A web front end, an API built from local source, and PostgreSQL:
services:
web:
image: nginx:1.27
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
api:
build: ./api # build from ./api/Dockerfile
image: shop/api:dev # tag the built image with this name
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/shop
depends_on:
- db
db:
image: postgres:${POSTGRES_VERSION:-16}
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: shop
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
And a .env file next to it (git-ignored):
DB_PASSWORD=devonly-change-me
POSTGRES_VERSION=16
What Compose does with this:
- Creates a network
<project>_default. The project name defaults to the folder name. - Creates the volume
<project>_db-data. - Builds
api, pullsnginxandpostgres, and starts the containers. - Every service is reachable by its service name: the API connects to
db:5432, nginx proxies tohttp://api:8000. - Only
webpublishes a port. The database is not reachable from outside the host.
Run it
$ docker compose up -d --build
[+] Running 5/5
✔ Network shop_default Created
✔ Volume "shop_db-data" Created
✔ Container shop-db-1 Started
✔ Container shop-api-1 Started
✔ Container shop-web-1 Started
$ docker compose ps
NAME IMAGE SERVICE STATUS PORTS
shop-api-1 shop/api:dev api Up 5 seconds 8000/tcp
shop-db-1 postgres:16 db Up 6 seconds 5432/tcp
shop-web-1 nginx:1.27 web Up 5 seconds 0.0.0.0:8080->80/tcp
$ docker compose logs -f api
$ docker compose exec db psql -U app -d shop -c '\dt'
Change and re-apply
Compose is declarative: edit the file and run up -d again. It compares what you asked for with what's running and recreates only the services that changed.
$ sed -i 's/nginx:1.27/nginx:1.27-alpine/' compose.yaml
$ docker compose up -d
✔ Container shop-db-1 Running
✔ Container shop-api-1 Running
✔ Container shop-web-1 Started # only web was recreated
docker compose restart does not pick up file changes; it restarts the existing containers as they are.
Environment variables, two different things
This trips people up, so be precise:
| Where | What it does |
|---|---|
${VAR} in compose.yaml |
Substitution when Compose reads the file, from your shell or the .env file |
environment: |
Variables set inside the container |
env_file: [ api.env ] |
Load container variables from a file |
Run docker compose config to see the final file with every variable filled in. It's the fastest way to debug "why is this value empty?". Be careful where you paste its output: it contains your secrets.
Try it: bring up the stack
- Create the folder layout:
compose.yaml,.env,nginx.conf(proxy/tohttp://api:8000) and anapi/folder with any small web app and its Dockerfile from lesson 2. docker compose up -d --build, thencurl localhost:8080.docker compose exec api getent hosts dbshows service-name DNS at work.docker compose down, thenup -dagain: the database data is still there. Now trydown -vand see it disappear.
Going deeper: project names and multiple copies
Two checkouts of the same repo in folders with the same name collide, because the project name (and so the container, network and volume names) comes from the folder name. Set it explicitly with name: shop at the top of compose.yaml, or -p shop-feature-x on the command line, to run several isolated copies of one stack on the same host, for example one per pull request on a CI runner.
Recap
- One compose.yaml describes services, networks and volumes. Use
docker compose(v2). - Compose creates a project network: services talk by service name. Publish only the entry point.
- Edit the file, then
docker compose up -d. It recreates only what changed. ${VAR}= substitution from shell/.env;environment:= variables in the container. Check withdocker compose config.downkeeps volumes,down -vdeletes them.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.