Skip to content
Home
Docker Compose: The Complete Guide

Docker Compose: The Complete Guide

Docker Docker 7 min read 1474 words Beginner ExcellentWiki Editorial Team

Docker Compose lets you define and run multi-container Docker applications with a single command. Instead of remembering and typing multiple docker run commands every time you start your development environment, you declare all services, networks, volumes, and environment variables in a YAML file and launch everything with docker compose up.

For development teams, Docker Compose solves the “it works on my machine” problem. Every developer gets the same database, cache, API server, and frontend running in identical containers, eliminating discrepancies between environments. For production, Compose simplifies deployment of modest multi-service applications and serves as a stepping stone to full orchestration with Docker Swarm or Kubernetes.

What Is Docker Compose?

Instead of running multiple docker run commands:

docker run -d --name db postgres:16
docker run -d --name api --link db my-api
docker run -d --name frontend --link api nginx

You define everything in a YAML file:

# docker-compose.yml
services:
  db:
    image: postgres:16
  api:
    build: ./api
    depends_on:
      - db
  frontend:
    build: ./frontend
    ports:
      - "80:80"

And start everything with one command: docker compose up. Docker Compose creates an isolated network for your application where services can reach each other by their service name (e.g., api can connect to db using db as the hostname). No more --link flags, manual network creation, or juggling container IDs.

Basic Structure

A Compose file has three top-level sections — services, networks, and volumes:

services:
  # Define your containers here

networks:
  # Define custom networks

volumes:
  # Define persistent volumes

The services section defines each container image, build context, ports, environment variables, volumes, and dependencies.

Defining Services

Each service maps to a container. Here is the full range of configuration options:

services:
  web:
    build: .                    # build from Dockerfile
    image: my-app:latest        # or use existing image
    ports:
      - "8000:8000"             # host:container
    environment:
      - DEBUG=true
      - DB_HOST=db
    env_file:
      - .env
    volumes:
      - ./src:/app/src          # bind mount
      - app_data:/data          # named volume
    depends_on:
      - db
      - redis
    restart: unless-stopped

Use build when your service has a custom Dockerfile and image when pulling a pre-built image from a registry. You can combine both — the image field then tags the built image, which is useful when you want to push it to a registry after building.

Environment Variables

Compose supports three ways to pass environment variables:

  1. Inline with environment: (good for static, non-sensitive values).
  2. File-based with env_file: (good for grouping related variables).
  3. Shell variable substitution in the YAML itself: ${DB_PASSWORD} (good for secrets loaded from .env).

Never hardcode secrets in docker-compose.yml. Use .env files (auto-loaded from the project directory) or Docker secrets for production.

Docker Compose in Action

Development

For development, prioritize hot-reload and debuggable configurations:

services:
  api:
    build:
      context: ./api
      target: development
    volumes:
      - ./api:/app              # live reload
    environment:
      - DEBUG=1
    ports:
      - "8000:8000"
    command: uvicorn app:app --reload --host 0.0.0.0

Binding your source code as a volume (./api:/app) lets you edit files on your host and see changes reflected inside the container immediately. Combined with a development server that supports auto-reload (like uvicorn, nodemon, or webpack-dev-server), you get the same developer experience as running natively.

Production

In production, remove bind mounts, enable health checks, set restart policies, and use production-optimized Dockerfiles:

services:
  api:
    build:
      context: ./api
      target: production
    ports:
      - "8000:8000"
    environment:
      - DEBUG=0
    restart: always
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Health checks let Docker know when a service is actually ready. Docker Compose waits for a service’s health check to pass before starting dependent services when depends_on includes condition: service_healthy. This is critical for services that must wait for a database to be ready before accepting connections.

Complete Example

A typical web application with PostgreSQL, Redis, a Python API, and Nginx:

services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  redis:
    image: redis:7-alpine

  api:
    build: ./api
    depends_on:
      - db
      - redis
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/myapp
      REDIS_URL: redis://redis:6379

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - api

volumes:
  pgdata:

Named volumes (like pgdata) persist data across container restarts. Bind mounts (like ./nginx.conf:...) are convenient for configuration files but make the container dependent on the host filesystem. For production, copy configuration into the image at build time or use Docker configs.

Service Discovery

Within a Compose project, services resolve each other by name. The API connects to db:5432 and redis:6379 using the service names from the YAML file. Docker Compose sets up a default network where all services can communicate, or you can define multiple isolated networks.

Common Commands

Manage the application lifecycle:

docker compose up              # create and start
docker compose up -d           # in background
docker compose down            # stop and remove
docker compose down -v         # remove volumes too

docker compose logs            # view logs
docker compose logs -f api     # follow logs for a service

docker compose ps              # list running services
docker compose exec api bash   # shell into a service

docker compose build           # rebuild images
docker compose pull            # pull latest images

docker compose restart         # restart all services
docker compose restart api     # restart a single service

Useful Variants

  • docker compose up --build — rebuild images before starting (equivalent to build then up).
  • docker compose up --scale api=3 — run three instances of the API service (useful for load testing).
  • docker compose down --remove-orphans — remove containers for services no longer in the Compose file.
  • docker compose config — validate and view the resolved Compose configuration with all .env substitutions applied.

Multiple Environments

Use multiple Compose files to override settings per environment:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up
# docker-compose.override.yml (auto-loaded for development)
services:
  api:
    volumes:
      - ./api:/app
    environment:
      - DEBUG=1
# docker-compose.prod.yml
services:
  api:
    restart: always
    environment:
      - DEBUG=0

Docker Compose automatically loads docker-compose.override.yml alongside docker-compose.yml. The override file merges settings on top of the base file. For production, explicitly specify -f flags to avoid loading the development override. You can chain as many files as needed — later files override earlier ones.

Networks

By default, Compose creates one network for all services. Define multiple networks for better isolation:

services:
  api:
    networks:
      - frontend
      - backend
  db:
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

Putting databases on a backend-only network and reverse proxies on a frontend network reduces the attack surface — even if the API is compromised, the database process is still isolated from external access. This multi-network pattern is standard practice for production deployments.

Best Practices

  1. Use .env files — never hardcode secrets in docker-compose.yml. Reference them with ${VARIABLE} syntax and include a .env.example file in version control showing the required variables with placeholder values.

  2. Specify restart policiesunless-stopped for production ensures containers restart after crashes or host reboots. Use no (default) for one-off tasks and always for essential services.

  3. Use healthchecks — ensures services are actually ready before dependents start. Without health checks, depends_on only waits for a container to start, not for the process inside to accept connections.

  4. Pin image versionspostgres:16-alpine, not postgres:latest. Tagged versions prevent unexpected breaking changes when upstream images are updated. Use Dependabot or Renovate to automatically update tags.

  5. Keep dev and prod close — minimize differences between environments. Use multi-stage builds (target: development vs target: production) to address differences while sharing the same Dockerfile.

  6. Use .dockerignore — don’t send unnecessary files to the builder. Ignore node_modules, .git, __pycache__, .env, and build artifacts. This speeds up builds and reduces the security surface.

  7. Set resource limits — prevent a runaway container from starving the host.

  8. Use profiles — define optional services (like admin tools, testing databases) that only start when you pass the profile name: docker compose --profile dev up.

FAQ

What is the difference between docker-compose (v1) and docker compose (v2)?

docker-compose (with hyphen) is the original Python-based v1 tool. docker compose (without hyphen) is the newer v2 plugin integrated into the Docker CLI. Use docker compose for new projects — it is faster and receives active development.

How do I debug why a service isn’t starting?

Run docker compose logs <service-name> to see the service’s output. Add --no-log-prefix for cleaner output. For interactive debugging, run docker compose run --rm <service-name> sh to start a shell instead of the default command.

Can I use Docker Compose in production?

Yes, but Compose is designed for single-host deployments. For multi-host production, use Docker Swarm or Kubernetes. Compose files can be translated to Swarm stacks with minimal changes.

How do I wait for a database to be ready before starting my app?

Use depends_on with condition: service_healthy combined with a health check on the database service. Alternatively, add a retry loop in your application startup code.

Why are my environment variables not being substituted?

Ensure you have a .env file in the same directory as your docker-compose.yml. Shell variable expansion uses ${VARIABLE} syntax. For literal $ characters, use $$.


Related: Start with Docker for beginners and learn Dockerfile best practices.

Section: Docker 1474 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top