DevToolBoxGRATIS
Blog

Docker Compose Guide: Write docker-compose.yml Files โ€” Complete Guide

12 min readdi DevToolBox

TL;DR โ€” Docker Compose in 30 Seconds

Docker Compose lets you define and run multi-container applications with a single YAML file. Run docker compose up -d to start all services, docker compose down to stop them. Use named volumes for databases, bind mounts for source code, healthchecks + depends_on for startup ordering, andprofiles to separate dev/prod services. Pin image tags in production and always set restart policies.

Key Takeaways

  • A docker-compose.yml has four top-level keys: version, services, networks, and volumes
  • Use depends_on with condition: service_healthy to wait for real readiness, not just container start
  • Named volumes persist data; bind mounts sync host files into containers for development
  • The .env file is automatically loaded for variable substitution in Compose files
  • Override files (docker-compose.override.yml) layer dev settings on top of the base config
  • Profiles (profiles: [debug]) let you selectively start optional services
  • In production: pin image tags, set restart policies, define resource limits, and use health checks

What Is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. You describe your entire application stack โ€” web servers, databases, caches, message queues โ€” in a singledocker-compose.yml file, and then spin everything up with one command.

The current version is Docker Compose V2 (written in Go, bundled with Docker Desktop as docker compose). The older Python-based V1 (docker-compose with a hyphen) reached end-of-life in 2023. All examples in this guide use the V2 syntax.

Want to generate a docker-compose.yml visually? Try the free Docker Compose Generator on DevToolBox โ€” select services, configure options, and download a ready-to-use file in seconds.

docker-compose.yml Structure: The Four Top-Level Keys

Every Compose file can contain four top-level keys. Only services is required; the rest are optional.

# docker-compose.yml โ€” annotated skeleton

version: "3.9"   # optional in Compose V2; omit or set to "3.9"

services:        # required โ€” defines each container
  web:
    image: nginx:1.27-alpine
  db:
    image: postgres:16-alpine

networks:        # optional โ€” custom network definitions
  frontend:
  backend:

volumes:         # optional โ€” named volume declarations
  db_data:
Note on the version field: Since Docker Compose V2, the version key is informational only and does not affect behavior. Compose automatically supports all Compose Specification features. You can safely omit it in new files.

Top-Level Keys Reference

KeyRequiredPurpose
versionNoCompose Specification version (informational in V2)
servicesYesContainer definitions โ€” image, ports, volumes, env, etc.
networksNoCustom network declarations and driver options
volumesNoNamed volume declarations; can reference external volumes
configsNoConfig files mounted into services (Swarm-compatible)
secretsNoSensitive data injected as files in /run/secrets/

Common Service Definitions: Web, Database, Cache

The services section is where you spend most of your time. Each service maps to one container. Here is a realistic three-tier stack covering a Node.js web server, PostgreSQL database, and Redis cache:

services:

  # โ”€โ”€ Web application โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  web:
    build:
      context: .
      dockerfile: Dockerfile
      target: development          # multi-stage build target
    image: myapp:dev
    container_name: myapp_web
    ports:
      - "3000:3000"                # host:container
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://user:pass@db:5432/mydb
      REDIS_URL: redis://cache:6379
    volumes:
      - .:/app                     # bind mount for hot-reload
      - /app/node_modules          # anonymous volume to preserve modules
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped
    networks:
      - frontend
      - backend

  # โ”€โ”€ PostgreSQL database โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  db:
    image: postgres:16-alpine
    container_name: myapp_db
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - db_data:/var/lib/postgresql/data   # named volume
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # seed script
    ports:
      - "5432:5432"                # expose for local DB clients
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    networks:
      - backend

  # โ”€โ”€ Redis cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  cache:
    image: redis:7-alpine
    container_name: myapp_cache
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped
    networks:
      - backend

networks:
  frontend:
  backend:

volumes:
  db_data:
  redis_data:

Key Service Properties

PropertyDescription
imageDocker image to use. Specify a tag for reproducibility.
buildBuild from a Dockerfile. Can be a path string or an object with context, dockerfile, args, target.
portsPort mappings as "HOST:CONTAINER" strings. Omit HOST to assign random port.
environmentEnvironment variables as KEY=value list or KEY: value mapping.
volumesMount points as "SOURCE:TARGET:OPTIONS". Source can be host path, named volume, or omitted (anonymous).
networksList of networks the container joins.
depends_onStartup dependencies with optional condition checks.
restartRestart policy: no, always, on-failure, unless-stopped.
commandOverride the default CMD from the image.
entrypointOverride the ENTRYPOINT from the image.

Environment Variables and .env Files

Docker Compose offers four ways to supply environment variables to containers. Each has different use cases and security implications.

Method 1: Inline environment key

services:
  web:
    environment:
      NODE_ENV: production
      PORT: "3000"
      # List syntax also works:
      # - NODE_ENV=production
      # - PORT=3000

Method 2: .env file (automatic loading)

Compose automatically reads a file named .env in the same directory as your docker-compose.yml. Variables defined there are available for substitution throughout the Compose file:

# .env file (auto-loaded, NOT committed to git)
POSTGRES_USER=myuser
POSTGRES_PASSWORD=s3cur3p@ss
POSTGRES_DB=mydb
APP_PORT=3000
# docker-compose.yml using .env substitution
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
  web:
    ports:
      - "${APP_PORT}:3000"

Method 3: env_file key

The env_file key loads all variables from a file directly into the container environment. Unlike the .env approach, these variables are not available for Compose file substitution โ€” they go straight into the container:

services:
  web:
    env_file:
      - .env              # loaded into container
      - .env.local        # merged on top (later files win)
      - path: .env.prod   # long form with required: true/false
        required: false

Method 4: Shell environment pass-through

# When you list a key with no value, Compose reads it from the shell
services:
  web:
    environment:
      - SECRET_KEY         # value comes from shell: export SECRET_KEY=...
      - DATABASE_URL
Security tip: Never hardcode secrets in docker-compose.yml if it is committed to version control. Use .env (gitignored) or Docker Secrets. Add .env to your .gitignore immediately.

Networking Between Containers

By default, Compose creates a single default network for your project. All services on that network can reach each other using the service name as the hostname. No need to know IP addresses.

# Containers on the same network communicate by service name:
# From the 'web' container, connect to PostgreSQL at:
#   host: db
#   port: 5432 (the container port, not the published host port)
DATABASE_URL=postgres://user:pass@db:5432/mydb

Custom Networks

Define multiple networks to isolate services. A common pattern is frontend(app + proxy) and backend (app + db + cache). The proxy can only reach the app; the database is completely isolated from the frontend:

services:
  nginx:
    image: nginx:alpine
    networks:
      - frontend       # proxy faces public internet

  web:
    image: myapp:latest
    networks:
      - frontend       # receives traffic from nginx
      - backend        # talks to db and cache

  db:
    image: postgres:16-alpine
    networks:
      - backend        # only reachable from web

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true     # no external internet access

Network Drivers

DriverUse Case
bridgeDefault. Container-to-container on the same host.
hostContainer shares host network stack. No isolation.
overlayMulti-host networking (Docker Swarm).
noneDisables networking completely.

Connecting to an External Network

# Reference a network created outside Compose
networks:
  shared_infra:
    external: true   # Compose will not create this network
    name: my_infra_network

Volumes: Named Volumes vs Bind Mounts

Volumes provide persistent storage that survives container restarts and removals. Docker Compose supports three volume types: named volumes, bind mounts, and anonymous volumes (deprecated in favor of named volumes for persistence).

Named Volumes (Production Recommended)

Named volumes are managed by Docker, stored in /var/lib/docker/volumes/, and are the recommended approach for database data and other persistent state:

services:
  db:
    image: postgres:16-alpine
    volumes:
      - db_data:/var/lib/postgresql/data  # named volume

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  db_data:           # declare at top level
    driver: local
  redis_data:
    driver: local

# Commands:
# docker volume ls                       List all volumes
# docker volume inspect myproject_db_data  Inspect a volume
# docker volume rm myproject_db_data     Remove a volume (after down)
# docker compose down -v                 Remove containers AND volumes

Bind Mounts (Development Recommended)

Bind mounts map a host directory or file into the container. Perfect for development because code changes on your host are immediately visible inside the container:

services:
  web:
    image: node:20-alpine
    volumes:
      - .:/app                    # entire project into /app
      - /app/node_modules         # anonymous vol: preserve container modules
    working_dir: /app
    command: npm run dev

  # Mount a single config file (read-only):
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro  # :ro = read-only

Volume Mount Options

# Long-form volume syntax (more explicit):
volumes:
  - type: bind
    source: ./src
    target: /app/src
    read_only: false
  - type: volume
    source: db_data
    target: /var/lib/postgresql/data
    volume:
      nocopy: true    # don't copy container data to volume on create
Named vs Bind summary: Use named volumes for database files, logs, and any data that must persist and be managed by Docker. Use bind mounts for source code in development where you want live editing. Never use bind mounts for database data in production โ€” file permission mismatches and filesystem performance issues can cause problems.

Health Checks

Health checks allow Docker to determine whether a container is actually ready to serve requests, not just running. A container can be in one of three states: starting,healthy, or unhealthy.

services:
  # PostgreSQL with pg_isready health check
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: myuser
      POSTGRES_DB: mydb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s      # how often to run the check
      timeout: 5s        # how long to wait for a response
      retries: 5         # how many consecutive failures before unhealthy
      start_period: 30s  # grace period before checks start counting

  # Redis with redis-cli ping
  cache:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

  # HTTP service health check using wget
  api:
    image: myapi:latest
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # HTTP service using curl
  web:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Health Check test Formats

FormatBehavior
["NONE"]Disables health check inherited from image.
["CMD", "cmd", "arg"]Runs command directly without a shell.
["CMD-SHELL", "cmd arg"]Runs command inside a shell (/bin/sh -c).

depends_on with Conditions

The depends_on key controls startup order between services. The simple form (list of service names) only ensures containers start in order โ€” it does NOT wait for a service to be ready. Use the long form with conditions for true readiness waiting.

Simple depends_on (start order only)

services:
  web:
    depends_on:
      - db
      - cache
  db:
    image: postgres:16-alpine
  cache:
    image: redis:7-alpine

depends_on with condition: service_healthy

This is the recommended approach for databases and services that take time to initialize. The dependent service starts only when the dependency's healthcheck returns healthy:

services:
  web:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy    # wait for db healthcheck to pass
        restart: true                 # restart web if db restarts
      cache:
        condition: service_started    # just wait for container to start
      migrations:
        condition: service_completed_successfully  # wait for job to finish

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

  cache:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]

  migrations:
    image: myapp:latest
    command: npm run migrate
    depends_on:
      db:
        condition: service_healthy

Condition Values Reference

ConditionWaits Until
service_startedContainer has started (no readiness guarantee). Default.
service_healthyContainer healthcheck reports healthy.
service_completed_successfullyContainer exited with code 0 (for one-off jobs).

Profiles: Dev vs Prod Services

Profiles let you define optional services in a single Compose file. Services assigned to a profile only start when that profile is explicitly activated. Services with no profile always start. This eliminates the need for separate Compose files for basic dev/prod differences.

services:
  # Core services โ€” always start
  web:
    image: myapp:latest
    ports:
      - "3000:3000"

  db:
    image: postgres:16-alpine

  # Development only tools โ€” start with --profile dev
  adminer:
    image: adminer:latest
    profiles: [dev]
    ports:
      - "8080:8080"
    depends_on:
      - db

  mailhog:
    image: mailhog/mailhog:latest
    profiles: [dev]
    ports:
      - "8025:8025"  # web UI
      - "1025:1025"  # SMTP

  # Monitoring stack โ€” start with --profile monitoring
  prometheus:
    image: prom/prometheus:latest
    profiles: [monitoring]
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    profiles: [monitoring]
    ports:
      - "3001:3000"

  # Debug tool โ€” start with --profile debug
  redis-commander:
    image: rediscommander/redis-commander:latest
    profiles: [debug]
    ports:
      - "8081:8081"

Activating Profiles

# Activate a single profile
docker compose --profile dev up -d

# Activate multiple profiles
docker compose --profile dev --profile monitoring up -d

# Via environment variable
export COMPOSE_PROFILES=dev,monitoring
docker compose up -d

# Run only a specific profiled service by name
docker compose run --rm adminer

Override Files: docker-compose.override.yml

Docker Compose automatically merges docker-compose.override.yml on top of docker-compose.yml when you rundocker compose up. This is ideal for developer-specific settings that should not be committed to version control.

Base file (docker-compose.yml) โ€” committed to git

# docker-compose.yml โ€” production-ready base
services:
  web:
    image: myapp:latest
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  db_data:

Override file (docker-compose.override.yml) โ€” gitignored

# docker-compose.override.yml โ€” developer overrides (gitignored)
services:
  web:
    build: .              # build from source in dev
    image: myapp:dev
    volumes:
      - .:/app            # live code reload
      - /app/node_modules
    environment:
      NODE_ENV: development
      DEBUG: "myapp:*"

  db:
    ports:
      - "5432:5432"       # expose DB port for local clients

Production override (docker-compose.prod.yml)

# docker-compose.prod.yml โ€” production layer
services:
  web:
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
# Use production override explicitly:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Merge order: later files win for scalars, arrays are concatenated

Merge Behavior

Key TypeMerge BehaviorExample
Scalars (image, command)Later file replaces earlier valueprod image tag overrides dev
Arrays (ports, volumes)Arrays are concatenatedAll ports from both files are added
Maps (environment)Keys are merged; later wins for conflictsOverride can add or change env vars

Common Docker Compose Commands

Here are the commands you will use daily, grouped by workflow:

Starting and Stopping

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

# Start with forced rebuild
docker compose up -d --build

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

# Stop all services (keeps containers and volumes)
docker compose stop

# Stop and remove containers (keeps volumes and images)
docker compose down

# Stop, remove containers, volumes, AND networks
docker compose down -v

# Remove everything including images
docker compose down -v --rmi all

Inspecting Services

# List running services and their status
docker compose ps

# List all (including stopped) services
docker compose ps -a

# View logs for all services
docker compose logs

# Follow logs in real time
docker compose logs -f

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

# Tail last 100 lines
docker compose logs --tail=100 web

Executing Commands

# Open an interactive shell in a running container
docker compose exec web sh
docker compose exec web bash

# Run a one-off command (creates a new container, removes on exit)
docker compose run --rm web npm run migrate
docker compose run --rm web npx prisma db push

# Run as a specific user
docker compose exec --user node web sh

# Copy a file from container to host
docker compose cp web:/app/logs/error.log ./error.log

Managing Images and Builds

# Build or rebuild images
docker compose build

# Build a specific service
docker compose build web

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

# Pull latest images
docker compose pull

# List images used by services
docker compose images

Scaling and Restarting

# Scale a service to N replicas (no resource limits needed)
docker compose up -d --scale worker=3

# Restart all services
docker compose restart

# Restart a single service
docker compose restart web

# View resource usage
docker compose top

Multi-Stage Builds with Docker Compose

Multi-stage builds reduce image sizes significantly by separating build dependencies from the final runtime image. Docker Compose works seamlessly with multi-stage Dockerfiles via the target build argument.

Multi-Stage Dockerfile

# Dockerfile
# โ”€โ”€ Stage 1: Install dependencies โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# โ”€โ”€ Stage 2: Build the application โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# โ”€โ”€ Stage 3: Development server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

# โ”€โ”€ Stage 4: Production runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY package*.json ./
EXPOSE 3000
CMD ["npm", "start"]

Compose targeting specific stages

# docker-compose.yml (base)
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      target: production     # use production stage
    image: myapp:latest
# docker-compose.override.yml (auto-merged in development)
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      target: development    # overrides to dev stage
      args:
        NODE_ENV: development
    image: myapp:dev
    volumes:
      - .:/app
      - /app/node_modules

Build args and secrets

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
        BUILD_DATE: ${BUILD_DATE}      # from shell env
        GIT_SHA: ${GIT_SHA}
      # BuildKit secret (doesn't leak into image layers)
      secrets:
        - npm_token

secrets:
  npm_token:
    environment: NPM_TOKEN            # reads from host shell

Production Best Practices

Docker Compose is well-suited for single-host production deployments. Follow these practices to make your Compose-based production deployment reliable, secure, and maintainable.

1. Pin Image Tags

# BAD: floating tags break reproducibility
services:
  db:
    image: postgres:latest    # changes unexpectedly
  cache:
    image: redis              # same as :latest

# GOOD: pin to a specific version
services:
  db:
    image: postgres:16.2-alpine  # exact patch version
  cache:
    image: redis:7.2-alpine

2. Restart Policies

services:
  web:
    restart: unless-stopped   # restart unless explicitly stopped
  db:
    restart: unless-stopped
  worker:
    restart: on-failure       # only restart on error exit codes
    # restart: always         # restart even on docker daemon restart

3. Resource Limits

services:
  web:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

4. Logging Configuration

services:
  web:
    logging:
      driver: json-file
      options:
        max-size: "10m"    # rotate when file reaches 10MB
        max-file: "5"      # keep 5 rotated files

5. Read-Only Containers

services:
  web:
    image: myapp:latest
    read_only: true            # filesystem is read-only
    tmpfs:
      - /tmp                   # allow writes to /tmp only
      - /var/run

6. Security: Drop Capabilities

services:
  web:
    image: myapp:latest
    cap_drop:
      - ALL                    # drop all Linux capabilities
    cap_add:
      - NET_BIND_SERVICE       # only add what you need
    security_opt:
      - no-new-privileges:true

7. Health Checks in Production

services:
  web:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s   # longer grace period in production

8. Secrets Management

# Using Docker Secrets (Swarm mode) or env files
services:
  db:
    image: postgres:16-alpine
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt  # not committed to git
    # external: true                 # reference externally created secret

Complete Production Example

# docker-compose.prod.yml โ€” production ready
services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_files:/var/www/static:ro
    depends_on:
      web:
        condition: service_healthy
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  web:
    image: myapp:1.5.2       # pinned version
    env_file: .env.prod
    volumes:
      - static_files:/app/public
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

  db:
    image: postgres:16.2-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    env_file: .env.prod
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 2G

volumes:
  db_data:
  static_files:

Generate docker-compose.yml Instantly

Select services visually, configure options, and get a production-readydocker-compose.yml in seconds. Free, no signup.

Open Docker Compose Generator

Debugging Docker Compose Issues

Validate your Compose file

# Check syntax and print the merged config
docker compose config

# Validate without printing
docker compose config -q && echo "Config is valid"

Common error: port already in use

Error: Bind for 0.0.0.0:5432 failed: port is already allocated

# Find what's using the port:
lsof -i :5432
# or
ss -tulpn | grep 5432

# Kill the process or change the host port in docker-compose.yml:
ports:
  - "5433:5432"    # map to a different host port

Common error: container exiting immediately

# Check logs for the crashed container
docker compose logs web

# Run interactively to debug
docker compose run --rm web sh

# Check exit code
docker compose ps -a

Inspect the merged configuration

# See exact configuration after all overrides are merged
docker compose -f docker-compose.yml -f docker-compose.override.yml config

# Useful for debugging profile interactions
docker compose --profile dev config

Frequently Asked Questions

What is the top-level structure of a docker-compose.yml file?

A docker-compose.yml file has four top-level keys: version (optional since Compose V2), services (required), networks (optional), and volumes (optional). The services section is where you define each container with its image, ports, environment, volumes, and other settings.

What is the difference between named volumes and bind mounts?

Named volumes (e.g. db_data:/var/lib/postgresql/data) are managed by Docker and stored in /var/lib/docker/volumes/. They persist across restarts and are recommended for production databases. Bind mounts (e.g. ./src:/app/src) map a host path into the container, ideal for development hot-reloading. Never use bind mounts for database data in production.

How does depends_on with condition: service_healthy work?

The long-form depends_on with condition: service_healthy makes Compose wait until the dependency's healthcheck reports healthy before starting the dependent service. You must define a healthcheck on the dependency. Withoutservice_healthy, depends_on only controls start order but does not wait for readiness.

How do I use .env files with Docker Compose?

Docker Compose automatically loads a .env file from the project directory. Variables there are available for substitution in the Compose file using${VAR_NAME} syntax. Use the env_file key to load variables directly into a container's environment. Always add .env to.gitignore to prevent committing secrets.

What are Docker Compose profiles and how do I activate them?

Profiles let you define optional services that only start when explicitly requested. Assign a profile with profiles: [dev]. Activate withdocker compose --profile dev up or setCOMPOSE_PROFILES=dev. Services with no profile always start. Common uses: debug tools, monitoring agents, mock services, test databases.

What is docker-compose.override.yml?

Compose automatically merges docker-compose.override.yml on top of docker-compose.yml. It is typically gitignored and contains developer-specific settings like bind mounts, extra ports, or debug commands. Arrays are concatenated; scalars in the override file replace the base file value.

What Docker Compose commands should I know?

Essential commands: docker compose up -d (start),docker compose down (stop and remove),docker compose logs -f (follow logs),docker compose exec service sh (shell access),docker compose ps (status),docker compose build (rebuild images),docker compose run --rm service cmd (one-off task),docker compose config (validate file).

What are production best practices for Docker Compose?

Pin image tags (never :latest), setrestart: unless-stopped, define resource limits underdeploy.resources, add health checks to all services, use env_file instead of hardcoding secrets, configurejson-file logging with size limits, use named volumes for persistent data, and separate dev/prod configs with override files.

Summary

Docker Compose is an essential tool for any developer working with containerized applications. With a single docker-compose.yml file, you can define complex multi-service stacks with databases, caches, reverse proxies, and background workers.

  • Structure: Four top-level keys โ€” services, networks, volumes, version
  • Services: Define web, db, and cache containers with image, ports, env, volumes
  • Environment: Use .env for substitution,env_file for container injection
  • Networking: Services reach each other by name; use custom networks for isolation
  • Volumes: Named volumes for persistence, bind mounts for dev hot-reload
  • Health checks: Use with depends_on: condition: service_healthy for true readiness
  • Profiles: Selectively start optional services with profiles: [name]
  • Override files: Layer dev settings on top of a committed base config
  • Production: Pin tags, set restart policies, limits, health checks, and external secrets

Ready to build your stack? Use the Docker Compose Generator to visually configure services and get a ready-to-use docker-compose.yml in seconds. Or explore related guides: the Docker Compose YAML errors guide and Docker volumes and bind mounts deep dive.

๐• Twitterin LinkedIn
รˆ stato utile?

Resta aggiornato

Ricevi consigli dev e nuovi strumenti ogni settimana.

Niente spam. Cancella quando vuoi.

Prova questi strumenti correlati

๐ŸณDocker Compose GeneratorYโ†’YAML to JSON Converter{ }JSON Formatter

Articoli correlati

Validazione YAML Docker Compose: 10 errori di sintassi comuni e come risolverli

Smetti di perdere tempo con errori YAML Docker Compose. Impara a identificare e correggere i 10 errori piรน comuni.

Docker Volumes vs Bind Mounts spiegati

Comprendi la differenza tra volumi Docker e bind mount. Casi d'uso, persistenza dei dati e strategie di backup.

Docker Compose Generator -- Build docker-compose.yml Online

Complete guide to generating docker-compose.yml files online. Learn Docker Compose file structure, services, networks, volumes, environment variables, health checks, depends_on, profiles, override files, common patterns, and production best practices.