The core idea
A multi-service app in Compose is the same set of containers you'd run by hand, but the relationships are written
down: which services share a network, which volume holds the data, which service is built from source. Compose turns
those relationships into the right docker calls, in the right order.
The notes stack as services
name: notes
services:
proxy:
image: nginx:alpine
ports: ["8080:80"]
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
networks: [frontend]
api:
build:
context: ./api
target: dev
environment:
DATABASE_URL: postgres://notes:notes@db:5432/notes
REDIS_URL: redis://cache:6379
networks: [frontend, backend]
db:
image: postgres:18
volumes:
- pgdata:/var/lib/postgresql
networks: [backend]
cache:
image: redis:8-alpine
networks: [backend]
networks:
frontend:
backend:
volumes:
pgdata:The full file, with healthchecks and startup order, is in project/docker/compose.yaml; the next two lessons fill in
the missing parts.
Things worth noticing:
- Service names are hostnames.
db,cacheandapiresolve through Docker's DNS, exactly like container names did in 05.02 User-defined bridges and DNS. That's whyDATABASE_URLsays@db:5432, and why the Nginx config now proxies tohttp://api:3000. - Networks are the security boundary.
apiis on both;proxycan't seedborcache. - Only
proxyhasports:. Services talk to each other over their networks without publishing anything. - Relative paths like
./nginx/default.confand./apiare relative to the compose file, so the project works wherever it's cloned. build:instead ofimage:for code you own. Compose builds it the first time, and again withdocker compose up --buildordocker compose build.
Working with one service
Most Compose commands take service names:
$ docker compose logs api --tail 3api-1 | notes-api listening on :3000 (max note length 280)$ docker compose exec db psql -U notes -c 'SELECT count(*) FROM notes'count-------3(1 row)$ docker compose run --rm --no-deps cache redis-cli -h cache pingPONG
execruns a command in the service's running container, likedocker exec.runstarts a new, one-off container from the service's definition, with its networks and environment.--rmremoves it afterwards and--no-depsskips starting its dependencies. It's how you run migrations, scripts or a one-time shell.restart api,stop cache,up -d proxyall work per service too.
The api service connects to postgres://notes:notes@localhost:5432/notes. Every other service is healthy, but the API can't reach the database. Why?
From project/docker (step 5, docker-step-05), run docker compose up -d. Use exec to count notes in Postgres, run --rm to ping Redis, and logs -f api while you curl -X POST localhost:8080/notes. Then docker compose stop cache and confirm the API still serves notes.
STUCK? ▸
proxy returns 502: the API is still starting, or isn't on frontend. docker compose logs api says which.
Recap
Interview questions
How do services in a Compose project communicate with each other?