Skip to content

Docker Compose Reference: compose.yaml, Commands, Volumes, Override Files & Networking

Docker Compose defines and runs multi-container applications using a single compose.yaml file. It’s the standard local development tool for microservices and the first step to understanding Kubernetes concepts — services, networks, and volumes map directly.

1. compose.yaml Structure

Services, images, ports, volumes, and environment variables
# compose.yaml (or docker-compose.yaml — Compose v2 prefers compose.yaml)
name: my-app

services:
  web:
    image: nginx:1.25-alpine
    ports:
      - "8080:80"             # host:container
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro  # bind mount, read-only
    depends_on:
      api:
        condition: service_healthy  # wait for health check, not just started
    networks: [frontend, backend]

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    env_file: .env             # load from .env file (don't commit .env to git)
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s        # grace period before health checks start
    depends_on: [db, redis]
    networks: [backend]
    deploy:
      resources:
        limits: {cpus: "0.5", memory: 512M}

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: pass
    volumes:
      - postgres_data:/var/lib/postgresql/data   # named volume (persisted)
    networks: [backend]

  redis:
    image: redis:7-alpine
    networks: [backend]

volumes:
  postgres_data:              # named volume — persisted across container restarts

networks:
  frontend:
  backend:                    # services on different networks can't talk to each other

2. Essential Commands

up, down, ps, logs, exec, build, and pull
# Start all services (detached):
docker compose up -d

# Start and rebuild images (after Dockerfile change):
docker compose up -d --build

# Start specific services only:
docker compose up -d db redis        # only start db and redis

# Stop all services (containers removed, volumes kept):
docker compose down

# Stop and remove named volumes (DESTRUCTIVE — wipes database):
docker compose down -v

# Check status:
docker compose ps

# Follow logs (all services):
docker compose logs -f

# Follow specific service logs:
docker compose logs -f api

# Run command in running container:
docker compose exec api sh
docker compose exec db psql -U postgres mydb

# Run one-off command in a new container (not running service):
docker compose run --rm api npm run migrate

# Rebuild a specific service image:
docker compose build api

# Pull latest base images:
docker compose pull

# Scale a service (run multiple instances):
docker compose up -d --scale api=3   # 3 api containers
# Requires: remove fixed 'ports' mapping (use load balancer instead)

# Restart a single service:
docker compose restart api

3. Volumes & Bind Mounts

Named volumes vs bind mounts, hot reload for development
# Named volume (managed by Docker — persists across restarts):
volumes:
  postgres_data:         # just declare it — Docker manages location

# Bind mount (host directory mapped into container — for dev):
volumes:
  - ./src:/app/src       # host path : container path
  - ./config.json:/app/config.json:ro   # :ro = read-only

# Dev hot-reload pattern (Node.js):
services:
  api:
    image: node:20-alpine
    working_dir: /app
    command: ["npm", "run", "dev"]     # nodemon or ts-node-dev watches for changes
    volumes:
      - .:/app                         # whole project synced into container
      - /app/node_modules              # anonymous volume: prevent host node_modules from overriding container's
    ports: ["3000:3000"]

# The /app/node_modules trick: anonymous volume "shadows" the bind mount at that path
# Container's node_modules are used, not host's (different OS/arch)

# Tmpfs mount (in-memory — good for tests):
services:
  test-db:
    image: postgres:16
    tmpfs: /var/lib/postgresql/data    # fast, ephemeral, no disk I/O

# Volume inspection:
docker volume ls
docker volume inspect my-app_postgres_data   # shows real path on host
docker volume rm my-app_postgres_data         # manual cleanup

4. Override Files & Environments

compose.override.yaml, multiple -f files, and environment-specific configs
# Docker Compose automatically merges compose.yaml + compose.override.yaml
# Use compose.override.yaml for dev settings (don't commit if personal):

# compose.yaml (base — production-like, minimal):
services:
  api:
    image: my-api:latest
    env_file: .env.production

# compose.override.yaml (dev overrides — auto-merged):
services:
  api:
    build: .                         # override image with local build
    volumes:
      - .:/app                       # hot-reload bind mount
    environment:
      - DEBUG=true
    command: npm run dev             # override to dev server

# Multiple -f files (explicit):
docker compose -f compose.yaml -f compose.staging.yaml up -d

# Environment-specific:
docker compose --env-file .env.staging up -d   # use specific .env file
# COMPOSE_FILE=compose.yaml:compose.staging.yaml   # env var alternative

# Profiles (opt-in services):
services:
  monitoring:
    image: prom/prometheus
    profiles: [monitoring]           # only starts with --profile monitoring
  app:
    image: my-app                    # no profile = always starts

docker compose --profile monitoring up -d    # starts app + monitoring
docker compose up -d                         # starts only app

5. Networking & Service Discovery

How containers find each other, custom networks, and external access
# Service discovery: containers talk via SERVICE NAME (not IP):
# From 'api' container:
curl http://db:5432       # postgres container (by service name)
redis-cli -h redis        # redis container
fetch("http://nginx/api") # nginx container

# Default network: all services share a default network if no 'networks' defined
# Custom networks: isolate services from each other

# Expose to host:
ports:
  - "8080:80"             # accessible at localhost:8080 on host
  - "127.0.0.1:5432:5432"  # ONLY on loopback — not accessible from network (security)
  - "0.0.0.0:5432:5432"    # accessible from any host interface (careful!)

# Container-only (no host exposure):
expose:
  - "3000"                # accessible to OTHER containers, not host

# Connect to external network (e.g., shared DB across projects):
networks:
  shared-db:
    external: true        # must exist: docker network create shared-db
  internal:               # created by compose

# Debugging networking:
docker compose exec api ping db                    # test connectivity by service name
docker compose exec api cat /etc/hosts             # see container DNS entries
docker network ls                                  # list networks
docker network inspect my-app_backend              # inspect a network

Track Docker and Docker Compose releases.
ReleaseRun monitors Kubernetes, Docker, and 13+ DevOps technologies.

Related: Docker Reference | Kubernetes YAML Reference | Traefik Reference | Docker EOL Tracker

🔍 Free tool: Docker Compose Security Checker — paste your docker-compose.yml and check 9 security misconfigurations — exposed ports, privileged containers, plain-text secrets — with an A–F grade.

Founded

2023 in London, UK

Contact

hello@releaserun.com