XplormityXplormity
HomeHandbooks
Browse
Xplormity

TLDR developer handbooks for
seasoned developers.

Handbooks

RustNestJSNext.jsGitDockerTypeScriptReactNode.jsDSASQLSystem DesignTailwind CSS

Site

HomeHandbooksAboutPrivacyTerms

Connect

GitHubTwitterLinkedIn

© 2026 Xplormity. All rights reserved.

HandbooksDockerDocker Compose v2 — Modern Multi-Container Apps

Docker Compose v2 — Modern Multi-Container Apps

composemulti-containerdevelopmenthandbook

TL;DR

  • Docker Compose v2 is a Docker CLI plugin (docker compose not docker-compose).
  • Define multi-container apps in compose.yaml (new default filename).
  • Profiles for environment-specific services (dev-only tools).
  • Health checks + depends_on conditions for proper startup ordering.
  • Secrets management built-in for sensitive data.

Step 1: Compose v2 Basics

Docker Compose was invented because managing multi-container applications with individual docker run commands is error-prone and unrepeatable. A single YAML file declares your entire application stack (database, backend, cache, worker) and brings it all up with one command. Compose v2 replaced the Python-based v1 with a Go plugin integrated directly into the Docker CLI, bringing better performance, BuildKit support, and features like profiles and watch mode that make local development feel seamless.

v1 vs v2

Feature v1 (Legacy) v2 (Current)
Command docker-compose up docker compose up
Config file docker-compose.yml compose.yaml
Implementation Python standalone binary Go plugin for Docker CLI
Performance Slower Faster
Features Limited Profiles, watch mode, include

Basic compose.yaml

# compose.yaml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:password@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./src:/app/src  # Hot reload in development

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

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

volumes:
  postgres_data:

Step 2: Health Checks & Dependency Ordering

Health checks exist because depends_on alone is dangerously misleading — it only waits for the container to start, not for the service inside to be ready. Your Node.js app starts in 200ms but PostgreSQL needs 3 seconds to accept connections, causing connection refused errors on first requests. Health checks + depends_on: condition: service_healthy solve this properly, and they're also what orchestrators like Kubernetes use to decide whether to route traffic to your container.

Without health checks, depends_on only waits for the container to start — not for the service inside to be ready.

depends_on Conditions

services:
  app:
    depends_on:
      db:
        condition: service_healthy      # Wait until healthy
      redis:
        condition: service_healthy
      migrations:
        condition: service_completed_successfully  # Wait until exit 0

Common Health Checks

# PostgreSQL
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 5s
  timeout: 3s
  retries: 5
  start_period: 10s

# Redis
healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 5s
  timeout: 3s
  retries: 5

# HTTP service
healthcheck:
  test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
  interval: 10s
  timeout: 5s
  retries: 3
  start_period: 15s

# MySQL
healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
  interval: 5s
  timeout: 3s
  retries: 5

Step 3: Profiles — Environment-Specific Services

Profiles solve the problem of having services that should only run in certain contexts: a debug dashboard in development, a metrics exporter in staging, or a seed-data service that runs once. Without profiles, teams maintained multiple compose files or commented services in and out. Profiles let you tag services and activate groups with --profile dev or --profile monitoring, keeping one source of truth for your entire stack.

Profiles let you define services that only run in specific contexts:

services:
  app:
    build: .
    ports: ["3000:3000"]
    # No profile = always runs

  db:
    image: postgres:16-alpine
    # No profile = always runs

  # Dev-only tools
  adminer:
    image: adminer
    ports: ["8080:8080"]
    profiles: ["dev"]  # Only runs with --profile dev

  mailhog:
    image: mailhog/mailhog
    ports: ["1025:1025", "8025:8025"]
    profiles: ["dev"]

  # Testing tools
  test-runner:
    build:
      context: .
      target: test
    profiles: ["test"]

  # Monitoring (production-like)
  prometheus:
    image: prom/prometheus
    profiles: ["monitoring"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

Usage

# Start core services only
docker compose up

# Start with dev tools
docker compose --profile dev up

# Start with multiple profiles
docker compose --profile dev --profile monitoring up

# Run tests
docker compose --profile test run test-runner

Step 4: Secrets Management

Secrets management in Compose exists because environment variables for sensitive data (passwords, API keys) are visible in docker inspect, process listings, and often end up in version control. Docker secrets mount sensitive values as files at /run/secrets/, which are tmpfs-backed (never written to disk), only accessible to services that declare them, and never appear in image layers or container metadata.

File-Based Secrets

services:
  app:
    build: .
    secrets:
      - db_password
      - api_key
    environment:
      # Read from mounted secret files at runtime
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Secrets are mounted as files at /run/secrets/<name> inside the container.

Reading Secrets in App

import { readFileSync } from "fs";

function getSecret(name: string): string {
  const secretPath = `/run/secrets/${name}`;
  try {
    return readFileSync(secretPath, "utf8").trim();
  } catch {
    // Fallback to environment variable in development
    return process.env[name.toUpperCase()] ?? "";
  }
}

const dbPassword = getSecret("db_password");

Step 5: Override Files & Environments

Override files let you maintain a single base docker-compose.yml while layering environment-specific changes on top. This was invented because development needs volume mounts and debug ports, staging needs different resource limits, and production needs replicas and health checks — but 90% of the service definitions are identical. Compose automatically merges docker-compose.override.yml on top of the base file, and you can chain multiple files with -f flags.

Development Override

# compose.yaml — base (production-like)
services:
  app:
    image: myapp:latest
    ports: ["3000:3000"]

# compose.override.yaml — auto-loaded in development
services:
  app:
    build: .
    volumes:
      - ./src:/app/src      # Hot reload
      - ./nodemon.json:/app/nodemon.json
    command: ["npm", "run", "dev"]
    environment:
      - NODE_ENV=development
      - DEBUG=app:*
# Development (auto-loads compose.override.yaml)
docker compose up

# Production (explicitly skip override)
docker compose -f compose.yaml up

# Custom override for CI
docker compose -f compose.yaml -f compose.ci.yaml up

Step 6: Watch Mode (Live Reload)

Watch mode was added in Compose v2.22 because bind-mounting source code with volumes has pain points: file permission issues, slow filesystem performance on macOS/Windows, and .node_modules conflicts. Watch mode uses file-syncing to push changes into running containers without volumes, and can trigger rebuilds or restarts based on which files changed. It gives you hot-reload developer experience with container isolation.

Docker Compose v2.22+ has watch for file-syncing without volume mounts:

services:
  app:
    build: .
    develop:
      watch:
        # Sync source code changes
        - action: sync
          path: ./src
          target: /app/src

        # Rebuild on dependency changes
        - action: rebuild
          path: ./package.json

        # Restart on config changes
        - action: sync+restart
          path: ./config
          target: /app/config
# Start with file watching
docker compose watch

Step 7: Full Production Stack Example

A production-ready compose file brings together everything: health checks ensuring dependency ordering, resource limits preventing runaway containers, restart policies for self-healing, named volumes for data persistence, networks for service isolation, and proper logging configuration. This example shows how a real application stack (API + database + cache + worker) is structured with all the production concerns addressed in one declarative file.

# compose.yaml — Production-ready 4-service stack
services:
  app:
    build:
      context: .
      target: runner
      args:
        NODE_ENV: production
    ports: ["3000:3000"]
    environment:
      DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/myapp
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    deploy:
      replicas: 2
      restart_policy:
        condition: on-failure
        max_attempts: 3
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  nginx:
    image: nginx:alpine
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      app:
        condition: service_healthy

volumes:
  postgres_data:
  redis_data:

Essential Commands

# Start services (detached)
docker compose up -d

# View logs
docker compose logs -f app

# Scale a service
docker compose up -d --scale app=3

# Rebuild after Dockerfile changes
docker compose up -d --build

# Stop everything
docker compose down

# Stop and remove volumes (⚠️ data loss)
docker compose down -v

# Execute command in running container
docker compose exec app sh

# Run one-off command
docker compose run --rm app npm test

Interview Questions

  1. What's the difference between depends_on and health checks?

    • depends_on alone only waits for container start. With condition: service_healthy, it waits until the health check passes — ensuring the service is actually ready.
  2. What are Compose profiles?

    • Labels on services that make them opt-in. Services without profiles always run. Services with profiles only start when that profile is activated with --profile.
  3. How do you manage secrets in Docker Compose?

    • Define secrets pointing to files. They're mounted at /run/secrets/ inside containers — never in environment variables or image layers.
  4. What's the difference between volumes and watch mode?

    • Volumes mount host directories directly (can have performance issues on macOS/Windows). Watch mode syncs files and can trigger rebuilds or restarts on specific changes.
Multi-Stage Builds & Image OptimizationSecurity & Production Hardening

On this page

  • TL;DR
  • Step 1: Compose v2 Basics
  • v1 vs v2
  • Basic compose.yaml
  • Step 2: Health Checks & Dependency Ordering
  • dependson Conditions
  • Common Health Checks
  • Step 3: Profiles — Environment-Specific Services
  • Usage
  • Step 4: Secrets Management
  • File-Based Secrets
  • Reading Secrets in App
  • Step 5: Override Files & Environments
  • Development Override
  • Step 6: Watch Mode (Live Reload)
  • Step 7: Full Production Stack Example
  • Essential Commands
  • Interview Questions