The core idea
Docker Compose reads a YAML file describing containers, networks and volumes, and makes reality match it. Instead of
remembering a dozen commands and their order, you write down what you want once and run docker compose up. It's
declarative: run it again and Compose only changes what's different.
From docker run to YAML
Every flag you've used has a key. Here's the database from step 4, both ways:
docker volume create notes-pgdata
docker run -d --name notes-db --network notes-backend \
-e POSTGRES_USER=notes -e POSTGRES_PASSWORD=notes -e POSTGRES_DB=notes \
-v notes-pgdata:/var/lib/postgresql postgres:18name: notes
services:
db:
image: postgres:18
environment:
POSTGRES_USER: notes
POSTGRES_PASSWORD: notes
POSTGRES_DB: notes
volumes:
- pgdata:/var/lib/postgresql
networks: [backend]
networks:
backend:
volumes:
pgdata:docker run | compose key |
|---|---|
| image name | image: |
docker build -t ... ./api | build: ./api (or build: { context, target }) |
-p 8080:80 | ports: ["8080:80"] |
-e KEY=value | environment: |
-v name:/path | volumes:, plus the volume under top-level volumes: |
--network name | networks:, plus the network under top-level networks: |
--restart unless-stopped | restart: unless-stopped |
--health-cmd ... | healthcheck: |
| command after the image | command: |
The four top-level keys
name: the project name. Compose prefixes everything it creates with it.services: one entry per container. The key (db) is the service name, and also its DNS name on the project's networks.networksandvolumes: declared once here, referenced from services. If you declare no networks at all, Compose creates one called<project>_defaultand puts every service on it.
The commands
$ docker compose up -dNetwork notes_frontend CreatedVolume notes_pgdata CreatedContainer notes-cache-1 CreatedContainer notes-db-1 CreatedContainer notes-api-1 CreatedContainer notes-proxy-1 Created...$ docker compose ps --format 'table {{.Service}}\t{{.Name}}\t{{.Status}}\t{{.Ports}}'SERVICE NAME STATUS PORTSapi notes-api-1 Up 5 seconds (healthy)cache notes-cache-1 Up 11 seconds (healthy) 6379/tcpdb notes-db-1 Up 11 seconds (healthy) 5432/tcpproxy notes-proxy-1 Up Less than a second 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp$ docker compose down
Output trimmed. Names follow the pattern project-service-number for containers and project_name for networks and
volumes.
| Command | Does |
|---|---|
docker compose up -d | create or update everything, start it in the background |
docker compose ps | the project's containers |
docker compose logs -f api | follow one service's logs (all services if you leave the name off) |
docker compose down | stop and remove containers and networks; keeps volumes |
docker compose down -v | the same, plus the volumes, which means the data |
Compose works from the folder containing compose.yaml (it also accepts the older name docker-compose.yml).
You change only the proxy's port in compose.yaml from 8080:80 to 9090:80 and run docker compose up -d again. What happens?
Write a compose.yaml with only the db service above. Run docker compose up -d, check the container, network and volume names with docker compose ps, docker network ls and docker volume ls, then docker compose down and confirm the volume survived.
STUCK? ▸
docker compose config prints the file as Compose understood it, after defaults and variables. If a key is missing there, it's indented wrong.
Recap
Interview questions
What is Docker Compose, and when would you use it instead of plain docker run?