Docker Compose

intermediate docker compose multi-container yaml

Running one container is easy. But real apps have multiple services — a web server, a database, a cache, maybe a worker process. Running each one manually with long docker run commands gets painful fast.

Docker Compose lets us define all our services in a single docker-compose.yml file and manage them together. One command to start everything. One command to stop everything.

The anatomy of a compose file

services:
  web:                              # service name (also the DNS hostname)
    build: .                        # build from Dockerfile in current dir
    ports:
      - "3000:3000"                 # host:container port mapping
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: postgres:16-alpine       # use a pre-built image
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pg-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pg-data:                          # named volume for DB persistence

Notice how the web service connects to the database using db as the hostname. Compose automatically creates a custom bridge network and sets up DNS for all services. The service name is the hostname.

Key configuration options

build — tells Compose to build an image from a Dockerfile instead of pulling one.

services:
  app:
    build:
      context: .                    # build context directory
      dockerfile: Dockerfile.prod   # non-default Dockerfile name

depends_on — controls startup order. But it only waits for the container to start, not for the service inside to be ready. A database container starting doesn’t mean PostgreSQL is accepting connections yet.

environment — two syntax options that do the same thing.

# List syntax
environment:
  - NODE_ENV=production
  - PORT=3000

# Map syntax
environment:
  NODE_ENV: production
  PORT: "3000"

env_file — loads environment variables from a file. Keeps secrets out of the compose file.

services:
  app:
    env_file:
      - .env

Essential commands

# Start all services (detached mode)
docker compose up -d

# Start and rebuild images
docker compose up -d --build

# Stop all services
docker compose down

# Stop and remove volumes too (careful — deletes data!)
docker compose down -v

# View logs
docker compose logs           # all services
docker compose logs -f web    # follow logs for one service

# Run a command in a running service
docker compose exec db psql -U user -d myapp

# Check status
docker compose ps

# Restart a single service
docker compose restart web

Profiles — optional services

Sometimes we want services that only run in certain situations. Profiles let us group services and start them selectively.

services:
  app:
    build: .
    ports:
      - "3000:3000"

  db:
    image: postgres:16-alpine
    volumes:
      - pg-data:/var/lib/postgresql/data

  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - debug                       # only starts with --profile debug

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
    profiles:
      - debug
# Normal start — adminer and mailhog won't run
docker compose up -d

# Start with debug services included
docker compose --profile debug up -d

A practical example

Here’s a compose file for a typical full-stack app — a Node.js API, PostgreSQL, and Redis. This is close to what real production setups look like.

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/myapp
      REDIS_URL: redis://cache:6379
      NODE_ENV: production
    depends_on:
      - db
      - cache
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - pg-data:/var/lib/postgresql/data
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
    restart: unless-stopped

volumes:
  pg-data:

The api service uses db and cache as hostnames — Compose’s built-in DNS handles the rest. The database data is persisted in a named volume so it survives restarts and deploys.