alltools.one
DevOpsβ€’
2025-06-16
β€’
8 min
β€’
alltools.one Team
YAMLDockerDocker ComposeDevOpsContainers

YAML for Docker Compose: Configuration Patterns and Tips

Docker Compose uses YAML to define multi-container applications. A well-structured docker-compose.yml is the difference between a smooth development environment and hours of debugging container issues. This guide covers essential patterns and common pitfalls.

Basic Structure

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
    depends_on:
      - api

  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: secret

volumes:
  pgdata:

Environment Variables

Inline

services:
  api:
    environment:
      NODE_ENV: production
      PORT: "3000"
      DEBUG: "false"

From File

services:
  api:
    env_file:
      - .env
      - .env.local  # Overrides .env values

Variable Substitution

Reference host environment variables:

services:
  api:
    image: myapp:${TAG:-latest}  # Default to "latest"
    environment:
      API_KEY: ${API_KEY}  # Required - fails if not set
      LOG_LEVEL: ${LOG_LEVEL:-info}  # Default to "info"

Networking

Custom Networks

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

networks:
  frontend:
  backend:
    internal: true  # No external access

Services on the same network can reach each other by service name. The db service is only reachable from api, not from web.

Port Mapping

ports:
  - "8080:80"         # host:container
  - "443:443"
  - "127.0.0.1:3000:3000"  # Bind to localhost only
  - "3000"            # Random host port

Volumes

Named Volumes (Persistent)

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    driver: local

Bind Mounts (Development)

services:
  api:
    volumes:
      - ./src:/app/src          # Source code
      - /app/node_modules       # Anonymous volume (prevent override)

The anonymous volume pattern (/app/node_modules) prevents the host bind mount from overwriting container-installed dependencies.

Health Checks

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    depends_on:
      db:
        condition: service_healthy

This ensures api only starts after db passes health checks β€” not just when the container starts.

Extension Fields (DRY Patterns)

Use x- prefixed fields with YAML anchors to reduce repetition:

x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"

x-env: &common-env
  TZ: UTC
  LOG_FORMAT: json

services:
  api:
    <<: *common
    build: ./api
    environment:
      <<: *common-env
      PORT: "3000"

  worker:
    <<: *common
    build: ./worker
    environment:
      <<: *common-env
      QUEUE: default

For more on YAML anchors, see our YAML anchors and aliases guide.

Multi-Stage Compose

Development vs Production

# docker-compose.yml (base)
services:
  api:
    build: ./api
    environment:
      DATABASE_URL: postgres://db:5432/myapp

# docker-compose.override.yml (dev - auto-loaded)
services:
  api:
    volumes:
      - ./api/src:/app/src
    environment:
      NODE_ENV: development
      DEBUG: "true"
    ports:
      - "3000:3000"
      - "9229:9229"  # Debug port

# docker-compose.prod.yml
services:
  api:
    environment:
      NODE_ENV: production
    deploy:
      replicas: 3
# Development (auto-loads override)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Common Pitfalls

1. Unquoted Port Numbers

# RISKY - YAML interprets 80:80 as a base-60 number
ports:
  - 80:80

# SAFE - always quote
ports:
  - "80:80"

2. Boolean Environment Variables

# WRONG - YAML converts to boolean
environment:
  DEBUG: true  # Becomes Python True, not string "true"

# CORRECT - quote all env values
environment:
  DEBUG: "true"

3. depends_on Timing

depends_on only waits for container start, not readiness:

# Container starts but DB might not be ready
depends_on:
  - db

# Wait for health check
depends_on:
  db:
    condition: service_healthy

Validate your Docker Compose files with our YAML Validator.

FAQ

What is the difference between docker-compose.yml and compose.yml?

Both file names work. compose.yml is the newer convention recommended by Docker. docker-compose.yml is the legacy name. Docker Compose v2+ looks for both, preferring compose.yml if both exist. For new projects, use compose.yml.

Should I use Docker Compose in production?

Docker Compose is excellent for development and simple deployments. For production at scale, consider Docker Swarm (uses Compose files natively) or Kubernetes. The advantage of Compose is that your development configuration directly informs production, reducing environment differences.

Related Resources

Published on 2025-06-16
YAML for Docker Compose: Configuration Patterns and Tips | alltools.one