Skip to main content
DevOps Insightsguides & answers

Docker Compose · guide

Docker Compose Guide

Compose defines and runs multi-container applications from a YAML file. One command builds, creates and starts every service. See the official Compose docs.

Last updated

On this page · 13 sections

1. What Is Docker Compose

Docker Compose lets you declare your entire application stack — services, networks, and volumes — in a single docker-compose.yml file. Instead of running multiple docker run commands, you run docker compose up.

Compose V2/V5 vs V1: Current Compose is invoked as docker compose (a Docker CLI plugin). Docker's supported Compose CLIs are v2 and v5 — v5, released in 2025, is functionally identical to v2, so docker compose version may report either. The old V1 was a standalone Python binary called docker-compose (with a hyphen) and is no longer supported. All examples in this guide use the docker compose syntax.


2. Install / Verify

Compose V2 is included with Docker Desktop. On Linux, it's installed as a plugin with the Docker Engine packages.

bash
# Check Compose is available
docker compose version

# If missing on Linux, install the plugin
sudo apt-get install -y docker-compose-plugin

3. Your First Compose File

Create a file named docker-compose.yml in your project directory. (compose.yaml is now the preferred name; Compose still reads docker-compose.yml for backwards compatibility and uses compose.yaml if both exist.) This example runs a Node.js app with a PostgreSQL database.

yaml
services:
  app:
    image: node:24-alpine
    working_dir: /app
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/appdb
    command: npm run dev
    depends_on:
      db:
        condition: service_healthy

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

volumes:
  pgdata:
bash
# Start everything
docker compose up -d

# Check it's running
docker compose ps

4. Services, Ports, and Environment Variables

Each top-level key under services: is a service. A service defines a container's image, ports, environment, and behavior.

yaml
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"        # host:container
      - "443:443"
    environment:
      - NGINX_HOST=example.com
      - NGINX_PORT=80

  api:
    image: my-api:latest
    ports:
      - "4000:4000"
    environment:
      NODE_ENV: production
      SECRET_KEY: my-secret

Port syntax is "HOST_PORT:CONTAINER_PORT". If you only specify one number ("3000"), Docker picks a random host port. Without a host IP the port is published on every interface (0.0.0.0), and Docker's rules bypass host firewalls such as ufw — use "127.0.0.1:3000:3000" for anything that should only be reachable from the host itself.


5. Volumes in Compose

Volumes persist data and share files between the host and containers. Compose supports both named volumes (managed by Docker) and bind mounts (direct host paths).

yaml
services:
  db:
    image: postgres:16-alpine
    volumes:
      # Named volume — Docker manages storage location
      - pgdata:/var/lib/postgresql/data
      # Bind mount — maps host directory into container
      - ./init-scripts:/docker-entrypoint-initdb.d

  app:
    image: my-app:latest
    volumes:
      # Bind mount for development (live reload)
      - ./src:/app/src
      # Anonymous volume to prevent overwriting node_modules
      - /app/node_modules

# Declare named volumes at the top level
volumes:
  pgdata:
bash
# List volumes created by compose
docker volume ls

# Remove volumes when tearing down (deletes data!)
docker compose down -v

6. Networking in Compose

Compose creates a default network for your project. All services can reach each other by service name as the hostname.

yaml
services:
  app:
    image: my-app:latest
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    networks:
      - backend

  nginx:
    image: nginx:alpine
    networks:
      - frontend

networks:
  frontend:
  backend:

In this setup, app can reach both db and nginx, but nginx cannot reach db directly — they are on separate networks.

bash
# Inspect the default network compose created
docker network ls
docker network inspect <project>_default

Service names act as DNS hostnames. From inside the app container, db resolves to the database container's IP.


7. depends_on and Health Checks

depends_on controls startup order. By default it only waits for the dependency to start, not to be ready. Combine with health checks to wait for actual readiness.

yaml
services:
  app:
    image: my-app:latest
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: example   # required by the postgres image
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

The app service will wait until db passes its health check and redis has started. start_period gives the service grace time before health checks count as failures.


8. Build Context

Instead of pulling a pre-built image, Compose can build images from a Dockerfile in your project.

yaml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
    ports:
      - "4000:4000"

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.prod
    ports:
      - "3000:80"
bash
# Build (or rebuild) images
docker compose build

# Build and start in one command
docker compose up -d --build

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

If you specify both build: and image:, Compose first tries to pull image and builds only if it can't be found (or when you pass --build); a built image is tagged with the image name. Set pull_policy: build to always build instead.


9. Environment Variables and .env Files

There are several ways to pass environment variables to services.

Inline in the compose file (fine for ordinary settings — for passwords and API keys prefer Compose secrets:, which mounts them as files under /run/secrets/ instead of exposing them as environment variables):

yaml
services:
  app:
    environment:
      NODE_ENV: production
      API_KEY: abc123

From a .env file loaded into the service:

yaml
services:
  app:
    env_file:
      - .env
      - .env.local

The project .env file — Compose automatically reads a .env file in the same directory as your compose file. Variables there can be referenced with ${VAR} syntax:

ini
# .env
POSTGRES_PASSWORD=supersecret
APP_PORT=3000
yaml
services:
  app:
    ports:
      - "${APP_PORT}:3000"
  db:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
bash
# Verify variable substitution
docker compose config

10. Override Files

Compose automatically merges docker-compose.yml with docker-compose.override.yml if both exist. This is perfect for dev-specific settings without modifying the base file.

docker-compose.yml (base — shared across environments):

yaml
services:
  app:
    image: my-app:latest
    ports:
      - "3000:3000"
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

docker-compose.override.yml (dev overrides — auto-loaded):

yaml
services:
  app:
    build: .
    volumes:
      - .:/app
    environment:
      NODE_ENV: development
bash
# Uses base + override automatically
docker compose up -d

# Use a specific override file
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Preview the merged config
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

11. Resource Limits

Without limits, one service in a stack can consume the whole host and take its neighbours down with it. Compose expresses limits under deploy.

yaml
services:
  app:
    image: node:24-alpine
    deploy:
      resources:
        limits:
          # A ceiling. The container is killed if it exceeds memory.
          cpus: "1.5"
          memory: 512M
        reservations:
          # Outside Swarm there is no scheduler: memory becomes a soft limit
          # (--memory-reservation) and cpus is ignored. Swarm uses both for placement.
          cpus: "0.25"
          memory: 128M

⚠️ The deploy key began as a Swarm-only feature, which is why a good deal of older material says plain docker compose up ignores it. Support has changed over releases, so rather than trusting either claim, confirm it on the version you are running:

bash
# What Compose actually resolved, after merges and variable substitution
docker compose config

# What the container really got — 0 means no limit was applied
docker inspect <container> --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}'

# And live usage against whatever limit is in force
docker stats --no-stream

💡 A container killed for exceeding its memory limit exits with code 137. See the Docker guide's resource-limits section for reading OOMKilled alongside that code — a 137 does not on its own prove the limit was the cause.


12. Production Patterns

When running Compose in production, add restart policies, resource limits, and logging configuration.

yaml
services:
  app:
    image: my-app:1.2.3
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      # The image will not start without it; Compose stops here if .env does not set it
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - pgdata:/var/lib/postgresql/data
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  pgdata:

Key production tips:

  • Always pin image tags (my-app:1.2.3, not :latest).
  • Use restart: unless-stopped so services survive reboots.
  • Set resource limits to prevent one service from starving others.
  • Configure log rotation to prevent disks from filling up.

Keep the database off the public network

A Compose file with one network puts every service on it, so a database that should only ever be reached by the application is reachable by anything else on that network — and, if you published its port, from outside the host.

Two networks fix it. The database joins only the internal one, which Compose will not attach to the host:

yaml
services:
  app:
    networks: [frontend, backend]
    ports:
      - "3000:3000"

  db:
    image: postgres:16-alpine
    networks: [backend]        # no ports: — nothing outside the compose network reaches it

networks:
  frontend:
  backend:
    internal: true             # no route to or from the host network

The check that matters is the absence of a ports: entry on db. Publishing a database port to debug something and leaving it published is an easy way for a development convenience to become an exposure.

13. Useful Commands

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

# Stop and remove containers, networks
docker compose down

# Stop and also remove volumes (deletes data!)
docker compose down -v

# View logs (all services)
docker compose logs -f

# View logs for a specific service
docker compose logs -f app

# List running services
docker compose ps

# Execute a command in a running service
docker compose exec app sh

# Run a one-off command (starts a new container)
docker compose run --rm app npm test

# Pull latest images
docker compose pull

# Rebuild images
docker compose build

# Restart a single service
docker compose restart app

# Validate and view the merged config
docker compose config

# Scale a service (if no host port conflicts)
docker compose up -d --scale worker=3
Terms used here (1)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Docker Compose
A tool for defining and running multi-container Docker applications using a YAML configuration file. With a single command you can spin up all the services your app needs — databases, caches, workers — in an isolated environment.

All 59 terms in the glossary