Skip to main content

Docker Compose Advanced Patterns

Patterns beyond basic service definitions — profiles, overrides, networking, secrets, and GPU passthrough.


Compose Profiles

Run subsets of services for different workflows:

services:
app:
build: .
profiles: [] # Always runs (no profile = default)

db:
image: postgres:16-alpine
profiles: [] # Always runs

redis:
image: redis:7-alpine
profiles: ["cache"] # Only with --profile cache

worker:
build: .
command: ["node", "worker.js"]
profiles: ["worker"] # Only with --profile worker

monitoring:
image: grafana/grafana
profiles: ["observability"] # Only with --profile observability

prometheus:
image: prom/prometheus
profiles: ["observability"]

Usage:

docker compose up                              # app + db only
docker compose --profile cache up # app + db + redis
docker compose --profile worker --profile cache up # app + db + redis + worker
docker compose --profile observability up # app + db + monitoring stack

Override Files

Layer configuration for different environments:

# docker-compose.yml (base)
services:
app:
build: .
environment:
NODE_ENV: production

# docker-compose.override.yml (auto-loaded in dev)
services:
app:
build:
target: development
volumes:
- .:/app
environment:
NODE_ENV: development
DEBUG: "app:*"

# docker-compose.staging.yml (explicit)
services:
app:
image: registry.example.com/app:staging
environment:
NODE_ENV: staging

Usage:

# Dev (auto-loads override)
docker compose up

# Staging (explicit file)
docker compose -f docker-compose.yml -f docker-compose.staging.yml up

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

Service Extensions (DRY)

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

x-healthcheck-defaults: &healthcheck-defaults
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

services:
api:
<<: *common
build: ./services/api
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]

worker:
<<: *common
build: ./services/worker
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "node", "-e", "process.exit(0)"]

Networking

Service Discovery

services:
api:
networks:
- frontend
- backend

db:
networks:
- backend # Only reachable from backend network

nginx:
networks:
- frontend # Only reachable from frontend network
ports:
- "80:80" # Exposed to host

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

Services on the same network can reach each other by service name (e.g., http://api:3000).

External Networks

# Connect to a network created outside compose
networks:
shared:
external: true
name: my-shared-network

Docker Compose Secrets

File-Based Secrets

services:
app:
secrets:
- db_password
- api_key

secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
environment: "API_KEY" # From env var (Compose v2.23+)

Inside the container, secrets are mounted at /run/secrets/<name>:

# Read secret in app
with open('/run/secrets/db_password') as f:
db_password = f.read().strip()

Anti-Pattern: Secrets in Environment

Wrong:

environment:
DB_PASSWORD: "hunter2" # Visible in docker inspect

Right: Use secrets: — they're mounted as files, not visible in container metadata.


GPU Passthrough

NVIDIA GPU

services:
ml-worker:
image: pytorch/pytorch:2.0-cuda11.7
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1 # or "all"
capabilities: [gpu]
environment:
NVIDIA_VISIBLE_DEVICES: all

Requires: nvidia-container-toolkit installed on host.

Apple Silicon (MPS)

Docker on macOS does NOT pass through Metal/MPS. For GPU workloads on Apple Silicon, run natively or use a VM with GPU passthrough.


Init Containers Pattern

Run setup tasks before the main service:

services:
db-migrate:
build: .
command: ["npx", "prisma", "migrate", "deploy"]
depends_on:
db:
condition: service_healthy

app:
build: .
depends_on:
db-migrate:
condition: service_completed_successfully
db:
condition: service_healthy

Volume Patterns

services:
app:
volumes:
# Named volume (persistent)
- pgdata:/var/lib/postgresql/data

# Bind mount (development hot reload)
- ./src:/app/src

# Anonymous volume (prevent host override)
- /app/node_modules

# tmpfs (in-memory, not persisted)
- type: tmpfs
target: /tmp
tmpfs:
size: 100000000 # 100MB

volumes:
pgdata:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres # Specific host path

Docker Compose Watch (2024+)

Faster than bind mounts for development:

services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- "**/*.test.ts"

- action: rebuild
path: package.json

- action: sync+restart
path: ./config
target: /app/config
ActionWhen
syncFile changes synced to container (hot reload)
rebuildContainer rebuilt from scratch
sync+restartFile synced, then container process restarted

Usage:

docker compose watch
# or
docker compose up --watch

Environment Variable Patterns

services:
app:
environment:
# Direct value
NODE_ENV: production

# From shell environment (required)
DATABASE_URL: ${DATABASE_URL}

# With default
PORT: ${PORT:-3000}

# From .env file (auto-loaded)
API_KEY: ${API_KEY}

env_file:
- .env # Always loaded
- .env.local # Overrides (gitignored)

.env file is auto-loaded by Compose. Variables defined in environment: take precedence over env_file:.


Debugging Compose

# Show resolved configuration
docker compose config

# Show service logs
docker compose logs -f app

# Execute command in running container
docker compose exec app sh

# Show resource usage
docker compose top

# Rebuild without cache
docker compose build --no-cache

# Remove everything (containers, volumes, networks)
docker compose down -v --remove-orphans