Claude-skill-registry compose-patterns-2025
Docker Compose production patterns 2025 including multi-environment strategies, health checks, and modern compose features
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/compose-patterns-2025" ~/.claude/skills/majiayu000-claude-skill-registry-compose-patterns-2025 && rm -rf "$T"
skills/data/compose-patterns-2025/SKILL.mdDocker Compose Patterns for Production (2025)
Overview
This skill documents production-ready Docker Compose patterns and best practices for 2025, based on official Docker documentation and industry standards.
File Format Changes (2025)
IMPORTANT: The
version field is now obsolete in Docker Compose v2.42+.
Correct (2025):
services: app: image: myapp:latest
Incorrect (deprecated):
version: '3.8' # DO NOT USE services: app: image: myapp:latest
Multiple Environment Strategy
Pattern: Base + Environment Overrides
compose.yaml (base):
services: app: build: context: ./app dockerfile: Dockerfile environment: - NODE_ENV=production restart: unless-stopped
compose.override.yaml (development - auto-loaded):
services: app: build: target: development volumes: - ./app/src:/app/src:cached environment: - NODE_ENV=development - DEBUG=* ports: - "9229:9229" # Debugger
compose.prod.yaml (production - explicit):
services: app: build: target: production deploy: replicas: 3 resources: limits: cpus: '1' memory: 512M restart_policy: condition: on-failure max_attempts: 3
Usage:
# Development (auto-loads compose.override.yaml) docker compose up # Production docker compose -f compose.yaml -f compose.prod.yaml up -d # CI/CD docker compose -f compose.yaml -f compose.ci.yaml up --abort-on-container-exit
Environment Variable Management
Pattern: .env Files per Environment
.env.template (committed to git):
# Database DB_HOST=sqlserver DB_PORT=1433 DB_NAME=myapp DB_USER=sa # DB_PASSWORD= (set in actual .env) # Redis REDIS_HOST=redis REDIS_PORT=6379 # REDIS_PASSWORD= (set in actual .env) # Application NODE_ENV=production LOG_LEVEL=info
.env.dev:
DB_PASSWORD=Dev!Pass123 REDIS_PASSWORD=redis-dev-123 NODE_ENV=development LOG_LEVEL=debug
.env.prod:
DB_PASSWORD=${PROD_DB_PASSWORD} # From CI/CD REDIS_PASSWORD=${PROD_REDIS_PASSWORD} NODE_ENV=production LOG_LEVEL=info
Load specific environment:
docker compose --env-file .env.dev up
Security Patterns
Pattern: Run as Non-Root User
services: app: image: node:20-alpine user: "1000:1000" # UID:GID read_only: true tmpfs: - /tmp - /app/.cache cap_drop: - ALL cap_add: - NET_BIND_SERVICE # Only if binding to ports < 1024 security_opt: - no-new-privileges:true
Create user in Dockerfile:
FROM node:20-alpine # Create app user RUN addgroup -g 1000 appuser && \ adduser -D -u 1000 -G appuser appuser # Set ownership WORKDIR /app COPY --chown=appuser:appuser . . USER appuser
Pattern: Secrets Management
Docker Swarm secrets (production):
services: app: secrets: - db_password - api_key secrets: db_password: file: ./secrets/db_password.txt api_key: external: true # Managed by Swarm
Access secrets in application:
// Read from /run/secrets/ const fs = require('fs'); const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
Development alternative (environment):
services: app: environment: - DB_PASSWORD_FILE=/run/secrets/db_password
Health Check Patterns
Pattern: Comprehensive Health Checks
HTTP endpoint:
services: web: healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 3s retries: 3 start_period: 40s
Database ping:
services: postgres: healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] interval: 10s timeout: 3s retries: 3
Custom script:
services: app: healthcheck: test: ["CMD", "node", "/app/scripts/healthcheck.js"] interval: 30s timeout: 3s retries: 3 start_period: 40s
healthcheck.js:
const http = require('http'); const options = { hostname: 'localhost', port: 8080, path: '/health', timeout: 2000 }; const req = http.request(options, (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => process.exit(1)); req.on('timeout', () => { req.destroy(); process.exit(1); }); req.end();
Dependency Management
Pattern: Ordered Startup with Conditions
services: web: depends_on: database: condition: service_healthy redis: condition: service_started migration: condition: service_completed_successfully database: healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: 10s redis: # No health check needed, just wait for start migration: image: myapp:latest command: npm run migrate restart: "no" # Run once depends_on: database: condition: service_healthy
Network Isolation Patterns
Pattern: Three-Tier Network Architecture
services: nginx: image: nginx:alpine networks: - frontend ports: - "80:80" api: build: ./api networks: - frontend - backend database: image: postgres:16-alpine networks: - backend # No frontend access networks: frontend: driver: bridge backend: driver: bridge internal: true # No external access
Pattern: Service-Specific Networks
services: web-app: networks: - public - app-network api: networks: - app-network - data-network postgres: networks: - data-network redis: networks: - data-network networks: public: driver: bridge app-network: driver: bridge internal: true data-network: driver: bridge internal: true
Volume Patterns
Pattern: Named Volumes for Persistence
services: database: volumes: - db-data:/var/lib/postgresql/data # Persistent data - ./init:/docker-entrypoint-initdb.d:ro # Init scripts (read-only) - db-logs:/var/log/postgresql # Logs volumes: db-data: driver: local driver_opts: type: none o: bind device: /mnt/data/postgres # Host path db-logs: driver: local
Pattern: Development Bind Mounts
services: app: volumes: - ./src:/app/src:cached # macOS optimization - /app/node_modules # Don't overwrite installed modules - app-cache:/app/.cache # Named volume for cache
Volume mount options:
- Read-only:ro
- Read-write (default):rw
- macOS performance optimization (host authoritative):cached
- macOS performance optimization (container authoritative):delegated
- SELinux single container:z
- SELinux multi-container:Z
Resource Management Patterns
Pattern: CPU and Memory Limits
services: app: deploy: resources: limits: cpus: '1.0' memory: 512M reservations: cpus: '0.5' memory: 256M
Calculate total resources:
# 3 app replicas + database + redis services: app: deploy: replicas: 3 resources: limits: cpus: '0.5' # 3 x 0.5 = 1.5 CPUs memory: 512M # 3 x 512M = 1.5GB database: deploy: resources: limits: cpus: '2' # 2 CPUs memory: 4G # 4GB redis: deploy: resources: limits: cpus: '0.5' # 0.5 CPUs memory: 512M # 512MB # Total: 4 CPUs, 6GB RAM minimum
Logging Patterns
Pattern: Centralized Logging
services: app: logging: driver: "json-file" options: max-size: "10m" max-file: "3" compress: "true" labels: "app,environment"
Alternative: Log to stdout/stderr (12-factor):
services: app: logging: driver: "json-file" options: max-size: "10m" max-file: "3"
View logs:
docker compose logs -f app docker compose logs --since 30m app docker compose logs --tail 100 app
Init Container Pattern
Pattern: Database Migration
services: migration: image: myapp:latest command: npm run migrate depends_on: database: condition: service_healthy restart: "no" # Run once networks: - backend app: image: myapp:latest depends_on: migration: condition: service_completed_successfully networks: - backend
YAML Anchors and Aliases
Pattern: Reusable Configuration
x-common-app-config: &common-app restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE services: app1: <<: *common-app build: ./app1 ports: - "8001:8080" app2: <<: *common-app build: ./app2 ports: - "8002:8080" app3: <<: *common-app build: ./app3 ports: - "8003:8080"
Pattern: Environment-Specific Overrides
x-logging: &default-logging driver: "json-file" options: max-size: "10m" max-file: "3" x-resources: &default-resources limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M services: app: logging: *default-logging deploy: resources: *default-resources
Port Binding Patterns
Pattern: Security-First Port Binding
services: # Public services web: ports: - "80:8080" - "443:8443" # Development only (localhost binding) debug: ports: - "127.0.0.1:9229:9229" # Debugger only accessible from host # Environment-based binding app: ports: - "${DOCKER_WEB_PORT_FORWARD:-127.0.0.1:8000}:8000"
Environment control:
# Development (.env.dev) DOCKER_WEB_PORT_FORWARD=127.0.0.1:8000 # Localhost only # Production (.env.prod) DOCKER_WEB_PORT_FORWARD=8000 # All interfaces
Restart Policy Patterns
services: # Always restart (production services) app: restart: always # Restart unless manually stopped (most common) database: restart: unless-stopped # Never restart (one-time tasks) migration: restart: "no" # Restart on failure only (with Swarm) worker: deploy: restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 120s
Validation and Testing
Pattern: Pre-Deployment Validation
#!/bin/bash set -euo pipefail echo "Validating Compose syntax..." docker compose config > /dev/null echo "Building images..." docker compose build echo "Running security scan..." for service in $(docker compose config --services); do image=$(docker compose config | yq ".services.$service.image") if [ -n "$image" ]; then docker scout cves "$image" || true fi done echo "Starting services..." docker compose up -d echo "Checking health..." sleep 10 docker compose ps echo "Running smoke tests..." curl -f http://localhost:8080/health || exit 1 echo "✓ All checks passed"
Complete Production Example
# Modern Compose format (no version field for v2.40+) x-common-service: &common-service restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" security_opt: - no-new-privileges:true services: nginx: <<: *common-service image: nginxinc/nginx-unprivileged:alpine ports: - "80:8080" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro networks: - frontend depends_on: api: condition: service_healthy healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] interval: 30s api: <<: *common-service build: context: ./api dockerfile: Dockerfile target: production user: "1000:1000" read_only: true tmpfs: - /tmp cap_drop: - ALL cap_add: - NET_BIND_SERVICE networks: - frontend - backend depends_on: migration: condition: service_completed_successfully redis: condition: service_started env_file: - .env healthcheck: test: ["CMD", "node", "healthcheck.js"] interval: 30s start_period: 40s deploy: resources: limits: cpus: '1' memory: 512M migration: image: myapp:latest command: npm run migrate restart: "no" networks: - backend depends_on: postgres: condition: service_healthy postgres: <<: *common-service image: postgres:16-alpine environment: - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password secrets: - postgres_password volumes: - postgres-data:/var/lib/postgresql/data networks: - backend healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: 10s deploy: resources: limits: cpus: '1' memory: 2G redis: <<: *common-service image: redis:7.4-alpine command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis-data:/data networks: - backend healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s networks: frontend: driver: bridge backend: driver: bridge internal: true volumes: postgres-data: driver: local redis-data: driver: local secrets: postgres_password: file: ./secrets/postgres_password.txt
Common Mistakes to Avoid
- Using
field - Obsolete in 2025version - No health checks - Leads to race conditions
- Running as root - Security risk
- No resource limits - Can exhaust host resources
- Hardcoded secrets - Use secrets or environment variables
- No logging limits - Disk space issues
- Bind mounts in production - Use named volumes
- Missing restart policies - Services don't recover
- No network isolation - All services can talk to each other
- Not using .dockerignore - Larger build contexts
Troubleshooting Commands
# Validate syntax docker compose config # View merged configuration docker compose config --services # Check which file is being used docker compose config --files # View environment interpolation docker compose config --no-interpolate # Check service dependencies docker compose config | yq '.services.*.depends_on' # View resource usage docker stats $(docker compose ps -q) # Debug startup issues docker compose up --no-deps service-name # Force recreate docker compose up --force-recreate service-name