Skip to content

Beginner · Docker · topic 18 of 53 · 35 min

Docker Compose: multi-container apps

Explanation

Real apps need several containers: a web app, a database, a cache. Docker Compose describes them all in one compose.yaml file, with their images, ports, environment, volumes, health checks and start order, and starts everything with docker compose up -d. Compose creates a shared network, so services reach each other by name. Be careful: docker compose down -v also deletes your volumes.

Why Compose?

Running a web app, Postgres and Redis by hand means three long docker run commands, a network, two volumes and the right order, typed the same way on every laptop. Docker Compose puts all of this in one YAML file in the repository. Anyone can clone the project and run one command to get the whole stack.

Compose v2 is a plugin of the Docker CLI, so the command is docker compose, with a space. The old standalone docker-compose (with a hyphen) is the retired v1. The preferred file name is compose.yaml (docker-compose.yml still works), and the old top-level version: key is obsolete: Compose ignores it, so leave it out.

A real example: a small e-commerce store

A small Jaipur startup sells block-printed fabrics online. Its stack is a Node.js storefront, PostgreSQL for orders and Redis for sessions and the cart.

yaml
services:
  web:
    build: .                       # build from the Dockerfile in this folder
    ports:
      - "8080:3000"
    env_file: .env                 # app settings, never committed
    environment:
      DATABASE_URL: postgres://shop:${POSTGRES_PASSWORD}@db:5432/shop
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy   # wait until Postgres answers
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: shop
      POSTGRES_DB: shop
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shop -d shop"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  cache:
    image: redis:7.4-alpine
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:
compose.yaml. ${POSTGRES_PASSWORD} is filled in from a .env file next to it or from your shell.

Reading the file

  • services: each entry becomes one or more containers. The key (web, db, cache) is also the host name other services use.
  • image or build: use a ready image, or build one from a Dockerfile.
  • ports: publish to the host, host first. Only web is published; db and cache are reachable only inside the Compose network, which is safer.
  • environment and env_file: set variables. Compose also reads a file called .env in the project folder to fill in ${VARIABLES} in compose.yaml.
  • volumes: the top-level section declares named volumes; the service section mounts them.
  • networks: Compose creates a default network for the project automatically. Add your own only when you need to separate groups of services.
  • depends_on: start order. The plain list form only waits for the container to START. With condition: service_healthy, Compose waits until that service's healthcheck passes.
  • healthcheck: the command Docker runs to decide whether the service is healthy.
  • restart: the same restart policies as docker run.
Think of it like this: compose.yaml is like the seating and menu plan for a wedding. Instead of telling each caterer separately when to arrive and where to set up, one plan lists every station, what it needs and who must be ready first. The tandoor must be hot (healthy) before the naan counter opens.

The commands

bash
docker compose up -d          # create network, volumes and containers; start in the background
docker compose up -d --build  # rebuild images first (after code or Dockerfile changes)
docker compose ps             # status and health of each service
docker compose logs -f web    # follow one service's logs
docker compose exec db psql -U shop shop   # run a command in a running service
docker compose build          # just build the images
docker compose down           # stop and remove containers and the network (volumes stay)
docker compose down -v        # ALSO delete the named volumes: your database is gone
Run these in the folder that holds compose.yaml.
Careful: docker compose down -v deletes the volumes declared in the file, which means your local database and Redis data. That is handy for a fresh start in development and a disaster anywhere else. Use plain down unless you really want to wipe the data.

Profiles and override files

Profiles let you include optional services only when asked. Services without a profile always start; a service with profiles starts only when you enable that profile.

yaml
  adminer:                      # a small web UI for the database
    image: adminer
    ports:
      - "8081:8080"
    profiles: ["debug"]
docker compose --profile debug up -d starts adminer too; plain up does not.

Override files change settings per environment. If a compose.override.yaml file exists, Compose merges it on top of compose.yaml automatically, which is handy for development extras such as bind-mounting the code. For other setups, list files explicitly: docker compose -f compose.yaml -f compose.prod.yaml up -d. Later files override earlier ones.

Common mistakes

  • Using depends_on without a healthcheck and expecting the database to be ready: the container may have started, but Postgres needs a few seconds before it accepts connections. Use condition: service_healthy, and still make the app retry its connection.
  • Connecting to localhost from the web container. Inside a container, localhost is the container itself; use the service name (db, cache).
  • Committing .env with real passwords. Commit a .env.example with dummy values and add .env to .gitignore.
  • Treating Compose as a production cluster. Compose is great for development, CI and small single-server deployments; for many servers and automatic failover, teams move to Kubernetes or a managed container service.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Docker Compose: multi-container apps · 1/7

Docker Compose

Docker Compose describes a multi-container app in one file and starts it with one command.

Prefer a video by a teacher? Find free videos on “docker compose tutorial postgres redis healthcheck depends_on” on YouTube ↗

In real life: One command to start a whole store

At the Jaipur fabric startup, the two developers and a freelance designer all run the store locally with the same compose.yaml. The designer, who never installed Postgres, runs docker compose up -d and opens localhost:8080. The CI pipeline uses the same file to start Postgres and Redis for integration tests, then runs docker compose down -v to throw the test data away. In production they currently run the same stack on one cloud VM with a compose.prod.yaml override that removes the bind mounts and adds memory limits.

bash
cp .env.example .env     # then set POSTGRES_PASSWORD
docker compose up -d
docker compose ps
docker compose logs -f web

Practice (0/3 done)

Task 1 · Question

In a compose.yaml, the database service is called db. What host name should the web service use to connect to it?

Task 2 · Code · Python

Compose must start services after the services they depend on. Write start_order(deps), where deps maps each service to the list of services it depends on. Work in rounds: in each round, start ALL services whose dependencies have already started, in alphabetical order. Return the full list. Raise ValueError if a dependency is not a known service, or if there is a cycle (no service can start but some are left).

Task 3 · Practice

Write a compose.yaml for a Python (FastAPI) attendance app for a coaching institute: an api service built from the local Dockerfile on port 8000, a PostgreSQL database and a Redis cache. The api must wait until Postgres is healthy. Data must survive docker compose down. Only the api should be reachable from the host. Add an optional pgadmin or adminer service under a "debug" profile.

Quick quiz

Question 1

1.What is the correct way to start a Compose v2 project in the background?

Question 2

2.What does depends_on: [db] (the short list form) guarantee?

Question 3

3.Which command removes the containers AND deletes the project's named volumes?

Question 4

4.You find version: "3.8" at the top of an old compose file. What should you know?

Question 5

5.A service has profiles: ["debug"]. When does it start?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the correct way to start a Compose v2 project in the background?

Answer: docker compose up -d

Compose v2 is the docker compose plugin (with a space); up -d creates and starts everything detached.

What does depends_on: [db] (the short list form) guarantee?

Answer: db is started before the dependent service is started

The short form only orders container start-up. Use condition: service_healthy with a healthcheck to wait for readiness.

Which command removes the containers AND deletes the project's named volumes?

Answer: docker compose down -v

The -v flag removes named volumes declared in the file and anonymous volumes. Plain down keeps them.

You find version: "3.8" at the top of an old compose file. What should you know?

Answer: It is obsolete and ignored by current Compose; you can remove it

The Compose Specification no longer uses the version key; current Compose ignores it (and warns).

A service has profiles: ["debug"]. When does it start?

Answer: Only when that profile is enabled, for example with --profile debug

Services with profiles start only when one of their profiles is active.

What does -p 8080:3000 mean in docker run?Related

Answer: Port 8080 on your machine goes to port 3000 inside the container

The format is host:container.

From Docker: packaging your app

What happens to files a container wrote into its own file system when you run docker rm on it?Related

Answer: They are deleted with the container

The writable layer belongs to the container. Use a volume for data that must survive.

From Running containers: ports, volumes, networks and limits

What is the full reference for the image you get with docker pull python?Related

Answer: docker.io/library/python:latest

The default registry is docker.io, official images live under library/, and the default tag is latest.

From Containers, images and registries

Why run the container as a non-root user?Related

Answer: It limits damage if the process is compromised

Least privilege is a basic security practice.

From Docker images for Go services

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Docker” before its exam: Image and container security.
  2. Module 4 exam: Docker · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Kubernetes fundamentals. 53 topics left in the course before the final exam.

← Previous: Running containers: ports, volumes, networks and limits