}

Docker Compose Getting Started Guide (2026): Complete Tutorial

Docker Compose Getting Started Guide (2026): Complete Tutorial

Last updated: March 2026

Docker Compose defines and runs multi-container applications from a single YAML file. In 2026, Compose v2 is built into the Docker CLI as docker compose (with a space). This complete tutorial covers installation, the compose.yml structure, a real-world multi-container application with a web app, PostgreSQL, and Redis, environment variables, health-check dependencies, and the commands you will use every day.


Docker Compose v1 vs v2

Before starting, clarify which version you are using:

Compose v1 Compose v2
Command docker-compose (hyphen) docker compose (space)
Implementation Standalone Python binary Go plugin built into Docker CLI
Status End-of-life July 2023 Current standard
File name docker-compose.yml compose.yml (also accepts docker-compose.yml)

Always use docker compose (v2) for new projects. Compose v1 is no longer maintained.


Installing Docker Compose v2

Ubuntu 22.04 / 24.04 / Debian 12

# Install the Docker Compose plugin
sudo apt-get update
sudo apt-get install -y docker-compose-plugin

# Verify
docker compose version
# Docker Compose version v2.24.0

RHEL 9 / CentOS Stream 9 / Fedora

sudo dnf install -y docker-compose-plugin
docker compose version

Using Docker Desktop (macOS / Windows / Linux)

Docker Desktop bundles Compose v2. No additional installation required.

docker compose version

Manual installation (any Linux)

# Install the latest release
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep tag_name | cut -d '"' -f 4)
sudo curl -SL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64" \
  -o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
docker compose version

The compose.yml File Structure

A Compose file has three top-level sections:

services:     # Container definitions
volumes:      # Named volumes
networks:     # Custom networks

The minimum valid Compose file:

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"

Run it:

docker compose up -d
docker compose ps
docker compose down

Service Configuration Reference

services:
  myapp:
    # Image to use (from registry or local)
    image: myapp:latest

    # Or build from a Dockerfile
    build:
      context: .
      dockerfile: Dockerfile
      target: production      # Multi-stage build target

    # Container name (optional)
    container_name: myapp

    # Port mapping (host:container)
    ports:
      - "8080:3000"
      - "127.0.0.1:9229:9229"   # Bind to localhost only

    # Environment variables
    environment:
      NODE_ENV: production
      PORT: "3000"

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

    # Volume mounts
    volumes:
      - ./src:/app/src              # Bind mount (dev)
      - app_data:/app/data          # Named volume (prod)

    # Networks
    networks:
      - frontend
      - backend

    # Dependency ordering
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

    # Restart policy
    restart: unless-stopped

    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

    # Healthcheck
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Real-World Example: Web App + PostgreSQL + Redis

This example defines a Python web application with a PostgreSQL database and Redis for caching/sessions.

# compose.yml
services:
  web:
    build: .
    container_name: webapp
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://appuser:apppassword@db:5432/appdb
      REDIS_URL: redis://redis:6379/0
      DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
      DEBUG: "false"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - static_files:/app/static
    networks:
      - app_network
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    container_name: postgres
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql   # Run on first start
    networks:
      - app_network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7-alpine
    container_name: redis
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - app_network
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - static_files:/var/www/static:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - web
    networks:
      - app_network
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  static_files:

networks:
  app_network:
    driver: bridge

Companion .env file:

# .env — never commit this file
DJANGO_SECRET_KEY=your-very-long-random-secret-key-here
POSTGRES_PASSWORD=apppassword

Add .env to .gitignore:

echo ".env" >> .gitignore

Essential Docker Compose Commands

Start and Stop

# Start all services in the background
docker compose up -d

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

# Start only specific services
docker compose up -d web db

# Stop all services (containers preserved)
docker compose stop

# Stop and remove containers, networks
docker compose down

# Stop and remove everything including volumes (CAUTION: deletes data)
docker compose down -v

# Stop and remove including images
docker compose down --rmi all

Status and Logs

# Show running services
docker compose ps

# Show all services including stopped
docker compose ps -a

# View logs (all services)
docker compose logs

# Follow logs
docker compose logs -f

# Logs for a specific service
docker compose logs -f web

# Last 50 lines
docker compose logs --tail 50 web

Execute Commands Inside Containers

# Open a shell in a running container
docker compose exec web bash
docker compose exec web sh    # Alpine containers use sh

# Run a one-off command
docker compose exec db psql -U appuser -d appdb

# Run a command in a new container (does not require the service to be running)
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py createsuperuser

Build and Pull

# Build all services
docker compose build

# Build a specific service
docker compose build web

# Build without using cache
docker compose build --no-cache web

# Pull latest images
docker compose pull

Scaling

# Run 3 instances of the web service
docker compose up -d --scale web=3
# Note: cannot scale services with specific container_name or host port bindings

Environment Variables in Compose

Docker Compose resolves environment variables from multiple sources in this order (later sources override earlier):

  1. Shell environment variables
  2. .env file in the same directory as compose.yml
  3. Environment files specified with env_file:
  4. Variables defined under environment: in the service definition
services:
  web:
    image: myapp:${APP_VERSION:-latest}    # Default to 'latest' if APP_VERSION not set
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      PORT: "3000"
    env_file:
      - .env
      - .env.${NODE_ENV:-production}       # e.g., .env.production
# Override at runtime
APP_VERSION=2.1.0 docker compose up -d

# Use a different env file
docker compose --env-file .env.staging up -d

depends_on and Service Ordering

depends_on controls start order and (with conditions) readiness:

depends_on:
  db:
    condition: service_healthy    # Wait for healthcheck to pass
  redis:
    condition: service_started    # Wait for container to start (not necessarily ready)
  migrations:
    condition: service_completed_successfully  # Wait for one-shot container to exit 0

The service_healthy condition requires a healthcheck on the dependency. Without it, service_started is used and the dependent container may start before the dependency is ready — leading to connection errors at startup.


Multiple Compose Files (Override Pattern)

Use multiple Compose files to handle environment differences:

# Base file
compose.yml

# Development overrides
compose.override.yml    # Automatically applied in development

# Production overrides
compose.prod.yml        # Apply explicitly with -f
# compose.override.yml (development)
services:
  web:
    volumes:
      - .:/app          # Live code mounting
    environment:
      DEBUG: "true"
    command: python manage.py runserver 0.0.0.0:8000
# Production (use explicit files)
docker compose -f compose.yml -f compose.prod.yml up -d

Related Articles


FAQ

Q: What is the difference between docker compose down and docker compose stop? docker compose stop stops the containers but preserves them and their data. You can restart with docker compose start. docker compose down stops the containers and removes them along with the networks Compose created. Named volumes are preserved unless you add -v. Use stop/start for quick pauses; use down/up when you want a clean slate.

Q: How do I run database migrations before the web app starts? Use a one-shot service that exits after running migrations:

services:
  migrations:
    build: .
    command: python manage.py migrate
    depends_on:
      db:
        condition: service_healthy
  web:
    build: .
    depends_on:
      migrations:
        condition: service_completed_successfully

Q: How do I pass secrets to Docker Compose without committing them? Use a .env file (never committed to version control) for development. In production, use Docker secrets (docker secret create) with Swarm mode, or pass secrets via environment variables from your CI/CD system (GitHub Actions secrets, GitLab CI variables, HashiCorp Vault). Reference them in compose.yml with ${SECRET_NAME} and inject them via the CI runner's environment.