Stage 2 · Multi-container apps · Chapter 11 of 20 · 5 min read

Docker Compose

Most real projects are several containers that must find each other. Compose describes the whole stack in one file and starts it with one command.

? What do the diagram colours mean

key idea / Docker itselfcontainer / good pathhost / watch outproblem / blockedneutral

Why Compose

A real app is several containers plus a network and a volume. Starting them by hand means remembering every flag, in the right order, on every machine.

Five commands become one fileOn the left, five docker commands create a network, a volume and three containers by hand. On the right, one compose.yaml file describes the same thing and docker compose up creates it.By hand, in the right order, every timedocker network create shopdocker volume create pgdatadocker run -d --network shop … dbdocker run -d --network shop … apidocker run -d -p 8080:80 … webreplaceDeclared oncecompose.yamlservices · networks · volumes$ docker compose up -d
Compose does not add new container features. It runs the same create and run steps for you, from a file you can commit.

Reading a compose file

Each key under services becomes a container. The keys inside are the docker run flags you already know, written as YAML.

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"            # the only door from outside
    depends_on: [api]

  api:
    build: ./api             # docker build ./api for you
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/shop
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}   # read from .env next to this file
      POSTGRES_DB: shop
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "app"]
      interval: 5s
      retries: 10

volumes:
  pgdata:
compose.yamlSame as
image: nginx:1.27docker run nginx:1.27
build: ./apidocker build ./api, then run the result
ports: ["8080:80"]-p 8080:80 (chapter 8)
environment:-e KEY=value (chapter 6)
volumes: [pgdata:/var/lib/…]-v pgdata:/var/lib/… (chapter 7)
(nothing)docker network create plus --network on every container

What it creates

What docker compose up createsCompose creates a project network named shop_default with three containers on it: web, api and db. Only web publishes a port. db stores its data in the volume shop_pgdata. Containers reach each other by service name.Browserlocalhost:8080network shop_default (created for you)webnginx :80apinode :3000dbpostgres :5432-p 8080:80http://api:3000db:5432shop_pgdatanamed volumeService names are DNS names on this network (chapter 10).Names get the project prefix: shop_default, shop_pgdata.
One command made a network, a volume and three containers, and wired names between them.

Start order is not readiness

depends_on: [db] only waits for the db container to start, not for Postgres to accept connections, which can take seconds. Give the dependency a healthcheck and wait for condition: service_healthy, as the file above does. Your app should still retry its first connection: databases restart in production too.

The commands you will use

CommandDoes
docker compose up -dcreate or update everything, in the background
docker compose up -d --buildrebuild images with build: first
docker compose psthis project’s containers and their health
docker compose logs -f apifollow one service’s logs
docker compose exec db psql -U app shopa shell or tool inside a running service
docker compose downremove containers and the network; keep volumes
docker compose down -valso delete the volumes, and your data
docker compose configprint the file with every ${VAR} filled in

Compose reads a .env file next to compose.yaml to fill in ${DB_PASSWORD}. Keep that file out of Git. A compose.override.yaml, if present, is merged on top automatically, which is a common place for development-only bind mounts.

Try it yourself

  1. Step 1 · Write a two-service file

    Work in an empty folder. The folder name becomes the project name.

    $ mkdir demo && cd demo
    $ cat > compose.yaml << 'EOF'
    services:
      web:
        image: nginx:1.27
        ports:
          - "8080:80"
      cache:
        image: redis:7-alpine
    EOF
  2. Step 2 · Bring it up

    Compose creates the network first, then both containers on it.

    $ docker compose up -d
     ✔ Network demo_default    Created
     ✔ Container demo-cache-1  Started
     ✔ Container demo-web-1    Started
    $ docker compose ps
    NAME           SERVICE   STATUS         PORTS
    demo-cache-1   cache     Up 3 seconds   6379/tcp
    demo-web-1     web       Up 3 seconds   0.0.0.0:8080->80/tcp
  3. Step 3 · Service names resolve

    web finds cache by name with no extra setup, because both are on demo_default.

    $ docker compose exec web getent hosts cache
    172.20.0.2      cache
    $ docker compose exec cache redis-cli ping
    PONG
    $ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080
    200
  4. Step 4 · Tear it down

    down removes what up made. There are no volumes here, so nothing is left behind.

    $ docker compose down
     ✔ Container demo-web-1    Removed
     ✔ Container demo-cache-1  Removed
     ✔ Network demo_default    Removed
Common mistakeCalling localhost:5432 from the api service to reach the database. Each service is its own container with its own localhost. Use the service name: db:5432.

Recap

Quick check

Your api starts before Postgres accepts connections and crashes, even with depends_on: [db]. Why?

Show answer

Answer: depends_on waits for the db container to start, not for Postgres to be ready.

Add a healthcheck to db and depends_on with condition: service_healthy, and let the app retry its first connection.

You run docker compose down -v. What happens to the pgdata volume?

Show answer

Answer: It is deleted, with all its data.

down removes containers and networks; -v also removes the named volumes declared in the file.

Docker Compose: common questions

Does depends_on wait for a database to be ready?

Not by itself. It waits for the container to start. Add a healthcheck to the database and use depends_on with condition: service_healthy.

Does docker compose down delete my data?

docker compose down removes containers and networks but keeps named volumes. docker compose down -v also deletes the volumes.