Skip to content

Join the Seedly owners community →

Docker

Docker Compose

Define and run multi-container applications with a compose.yaml file

Written by 13 min read1 activity
Pixl, your presenter

Pixl presents

Starting five containers by hand, one terminal each? Please stop. One compose file, one command, whole stack.Starting five containers by hand, one terminal each? Please stop. One compose file, one command, whole stack.

Pixl holds a plan and pulls one lever that opens and lights three shipping containers
One file and one command start the whole stack

Last lesson you stood up a web app, a database and a cache with a pile of docker run commands. It works... but it's tedious, and one typo means you're starting over. So what if you could describe the whole multi-container app in one file and start everything with one command? That's EXACTLY what Docker Compose does.

What is Docker Compose?#

Docker Compose is a tool for defining and running apps made of several containers. You describe all your services, networks and volumes in one YAML file, then run the whole thing with a couple of simple commands. The current docs call that file compose.yaml, but plenty of projects still use the older docker-compose.yml name and Compose happily reads either one.

# Start all services defined in your compose file
docker compose up
 
# Stop everything
docker compose down

Yep, that's it. One command starts your whole app stack.

Your First compose.yaml#

Here's a simple example with a web app and a PostgreSQL database.

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
    depends_on:
      - db
 
  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
 
volumes:
  pgdata:

Let's pick it apart piece by piece.

Services: Your Containers#

The services section lists every container your app needs. Each service gets a name (like web or db), and the other services can use that name as a hostname.

services:
  web:
    build: .                    # Build from Dockerfile in current directory
    ports:
      - "3000:3000"            # Map port 3000
 
  api:
    image: myapp-api:latest    # Use a pre-built image
    ports:
      - "8080:8080"

Ports: Expose Services#

The ports section maps ports on your machine to ports in the container, same deal as the -p flag.

services:
  web:
    ports:
      - "3000:3000"     # host:container
      - "9229:9229"     # You can map multiple ports (e.g., for debugging)

Environment Variables#

Here's how you set environment variables for your containers.

services:
  web:
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379

Or you can use an .env file and keep your secrets out of the compose file altogether.

services:
  web:
    env_file:
      - .env

Volumes: Persist Data#

Set up volumes so your data sticks around.

services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data    # Named volume
 
  web:
    build: .
    volumes:
      - ./src:/app/src                      # Bind mount for development
 
# Declare named volumes at the bottom
volumes:
  pgdata:

depends_on: Control Startup Order#

Pixl waves a flag to send a sturdy cart off first while a smaller cart waits
depends_on decides which service starts first

depends_on makes sure your services start in the right order.

services:
  web:
    build: .
    depends_on:
      - db
      - cache
    # web starts AFTER db and cache are running
 
  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=secret
 
  cache:
    image: redis:7
services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy
 
  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

Networks: Automatic Communication#

Docker Compose builds a network for your services automatically. Every service can reach every other one using its service name as a hostname, so most of the time you don't have to define networks at all.

services:
  web:
    environment:
      # "db" resolves to the db service automatically
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      # "cache" resolves to the cache service
      - REDIS_URL=redis://cache:6379
 
  db:
    image: postgres:16
 
  cache:
    image: redis:7

A Complete Real-World Example#

Here's a full-stack app with a Next.js frontend, an Express API, a PostgreSQL database and a Redis cache.

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - API_URL=http://api:8080
    depends_on:
      - api
 
  api:
    build: ./api
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    volumes:
      - ./api/src:/app/src    # Live reload in development
 
  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"           # Expose for local database tools
 
  cache:
    image: redis:7
    ports:
      - "6379:6379"
 
volumes:
  pgdata:

Essential Docker Compose Commands#

# Start all services (with live output)
docker compose up
 
# Start in the background
docker compose up -d
 
# Stop and remove all containers
docker compose down
 
# Stop and remove containers + volumes (deletes data!)
docker compose down -v
 
# Rebuild images before starting
docker compose up --build
 
# View logs for all services
docker compose logs
 
# View logs for one service
docker compose logs api
 
# Follow logs in real-time
docker compose logs -f
 
# List running services
docker compose ps
 
# Open a shell inside a running service
docker compose exec api sh
 
# Restart a specific service
docker compose restart api

Development vs Production#

You can use separate compose files for separate environments.

# Development (default)
docker compose up
 
# Production (with override file)
docker compose -f compose.yaml -f compose.prod.yaml up -d

A production override file might strip out the bind mounts, add resource limits and swap some environment variables.

TL;DR#

  • Docker Compose describes multi-container apps in one compose.yaml file (older projects call it docker-compose.yml)
  • services lists each container (web, db, cache, etc.)
  • ports maps ports on your machine to ports in the container
  • environment sets environment variables (use env_file for secrets)
  • volumes keeps data around and syncs your code live during development
  • depends_on controls the order services start in
  • Services find each other by name automatically (no IP addresses needed)
  • docker compose up starts everything, and docker compose down stops everything

What's Next?#

Your Docker setup is humming along now, but your images are probably bigger and slower to build than they need to be. Next lesson gets into slimming them down with tricks like multi-stage builds, .dockerignore and smarter layer caching, so your images shrink and your builds speed up...

This lesson ends with a short activity.