DevOpsDocker

Docker Compose Guide: Run Multi-Container Apps with One Command

Learn Docker Compose to define and run multi-container applications. Covers compose.yaml syntax, services, networks, volumes, environment files, and depends_on.

TT
Daniel Brooks
5 min read
Docker Compose Guide: Run Multi-Container Apps with One Command

Running a web application means running multiple containers: an API server, a database, maybe a cache layer. Managing all of these with individual docker run commands is tedious and error-prone. Docker Compose solves this by letting you define your entire application stack in a single YAML file, then start everything with one command.


What Is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. You describe your services, networks, and volumes in a compose.yaml file, then:

bash
docker compose up      # start all services
docker compose down    # stop and remove containers

Compose handles starting containers in the right order, creating a shared network so containers can talk to each other by service name, managing volumes for persistent data, and setting environment variables.


The compose.yaml Structure

yaml
services:
  service-name:
    image: image:tag
    # or
    build: ./path/to/dockerfile/directory
    ports:
      - "HOST:CONTAINER"
    environment:
      - KEY=VALUE
    volumes:
      - source:target
    depends_on:
      - other-service

volumes:
  volume-name:

networks:
  network-name:

The top-level keys are services, volumes, and networks. services is the only required section.


A Basic Example: API + Database

Here's a realistic stack: a Node.js API and a PostgreSQL database.

yaml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://myuser:secret@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres-data:

Start the stack:

bash
docker compose up

Both containers start. The api service waits for db to pass its health check before starting. The API can reach the database at the hostname db — Docker Compose creates a shared network and assigns each service's name as its DNS hostname.


Core compose.yaml Syntax

build vs image

yaml
services:
  api:
    build: .           # build from Dockerfile in current directory

  redis:
    image: redis:7-alpine  # use a pre-built image from Docker Hub

environment and env_file

yaml
# Inline environment variables
environment:
  NODE_ENV: development
  PORT: 3000

# Or from a .env file
env_file:
  - .env

The .env file in the same directory as compose.yaml is automatically read by Compose for variable substitution. Never commit .env to source control — add it to .gitignore.

volumes

yaml
services:
  db:
    volumes:
      - postgres-data:/var/lib/postgresql/data  # named volume (managed by Docker)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # bind mount

volumes:
  postgres-data:  # declares the named volume

Named volumes survive docker compose down by default. To remove them too: docker compose down -v.

depends_on

yaml
services:
  api:
    depends_on:
      db:
        condition: service_healthy   # wait until db health check passes
      redis:
        condition: service_started   # just wait for container to start

service_healthy waits until the container's healthcheck passes — much safer for databases.

healthcheck

yaml
healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

Essential Docker Compose Commands

CommandWhat It Does
docker compose upStart all services
docker compose up -dStart in background (detached)
docker compose up --buildRebuild images then start
docker compose downStop and remove containers + networks
docker compose down -vAlso remove volumes
docker compose psList service containers
docker compose logsView all service logs
docker compose logs -f apiFollow logs for the api service
docker compose exec api shShell into the running api container
docker compose restart apiRestart a single service
docker compose pullPull latest images

Development Workflow with Compose

For development, mount your source code as a volume so changes are reflected immediately without rebuilding:

yaml
services:
  api:
    build: .
    volumes:
      - .:/app              # mount source code
      - /app/node_modules   # anonymous volume prevents host node_modules overwriting
    command: npm run dev    # overrides Dockerfile CMD to use a watch-mode command

Complete Three-Service Stack

yaml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    env_file: .env
    environment:
      - NODE_ENV=development
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:16-alpine
    env_file: .env
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

volumes:
  postgres-data:

Previous: Lesson 4 — Dockerfile Guide | Next: Lesson 6 — What Is Kubernetes?


Part of the Docker & Kubernetes Mastery course.

External references: