Docker Compose bridges the gap between single-container development and full orchestration. These patterns will help you build maintainable, production-ready configurations.

Project Structure

myprodddd..sjooooeeeeccccnnrckkkkvvvteeee.iawn/rrrrecpog----xepriccccasknoooomDeDxDnmmmmpor/ogpppplccciooooekkknsssseeexeeeerrr.....fffcyoptiiiomvrelllnleoseeefrdtr..iyydmmell.yml######BDPTETaerenesvosvmedtipurlcvccoaoetontnrinmefrofeiinin(gdgtcueuorsvrvmaeaamt(rtriiariitouioatntdnbeoeld-se)lsoaded)

Base Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# docker-compose.yml
version: "3.8"

services:
  app:
    build:
      context: ./services/app
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - backend
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

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

networks:
  backend:
    driver: bridge

volumes:
  postgres_data:
  redis_data:

Development Overrides

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# docker-compose.override.yml (auto-loaded with docker-compose up)
version: "3.8"

services:
  app:
    build:
      target: development
    volumes:
      - ./src:/app/src:cached
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229"  # Debugger
    environment:
      - DEBUG=true
      - LOG_LEVEL=debug
    command: npm run dev

  db:
    ports:
      - "5432:5432"

  redis:
    ports:
      - "6379:6379"

Production Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# docker-compose.prod.yml
version: "3.8"

services:
  app:
    build:
      target: production
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./services/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
    networks:
      - backend
      - frontend

networks:
  frontend:
    driver: bridge

Run with:

1
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Multi-Stage Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# services/app/Dockerfile

# Base stage
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]

# Dependencies stage
FROM base AS deps
COPY package*.json ./
RUN npm ci --only=production && \
    cp -R node_modules /prod_modules && \
    npm ci

# Development stage
FROM base AS development
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000 9229
CMD ["npm", "run", "dev"]

# Build stage
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Production stage
FROM base AS production
ENV NODE_ENV=production
COPY --from=deps /prod_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]

Health Checks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

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

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

  nginx:
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 30s
      timeout: 10s
      retries: 3

Dependency Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  app:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
      migrations:
        condition: service_completed_successfully

  migrations:
    build: ./services/app
    command: npm run migrate
    depends_on:
      db:
        condition: service_healthy
    restart: "no"

Environment Variables

1
2
3
4
5
6
7
# .env.example
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=change_me
DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
REDIS_URL=redis://redis:6379
SECRET_KEY=change_me
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Using env_file
services:
  app:
    env_file:
      - .env
      - .env.local  # Optional overrides

# Variable substitution with defaults
services:
  app:
    environment:
      - PORT=${PORT:-3000}
      - LOG_LEVEL=${LOG_LEVEL:-info}

Networking Patterns

Internal Services

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
services:
  api:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend  # Not exposed to frontend

  nginx:
    networks:
      - frontend
    ports:
      - "80:80"

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

Custom Network Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
          gateway: 172.20.0.1

services:
  db:
    networks:
      backend:
        ipv4_address: 172.20.0.10

Volume Patterns

Named Volumes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
volumes:
  postgres_data:
    driver: local
  
  # With specific options
  uploads:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/uploads

Bind Mounts for Development

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
services:
  app:
    volumes:
      # Source code (cached for performance on Mac)
      - ./src:/app/src:cached
      
      # Exclude node_modules from bind mount
      - /app/node_modules
      
      # Read-only config
      - ./config:/app/config:ro

Shared Volumes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
services:
  app:
    volumes:
      - shared_tmp:/tmp/shared

  worker:
    volumes:
      - shared_tmp:/tmp/shared

volumes:
  shared_tmp:

Logging Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        labels: "app,environment"
        env: "NODE_ENV"

  # Or send to external service
  app-prod:
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://logs.example.com:514"
        tag: "myapp/{{.Name}}"

Resource Limits

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 256M

  # For docker-compose (non-swarm)
  worker:
    mem_limit: 512m
    cpus: 1

Testing Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# docker-compose.test.yml
version: "3.8"

services:
  app:
    build:
      target: development
    command: npm test
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgres://test:test@db-test:5432/test

  db-test:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: test
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    tmpfs:
      - /var/lib/postgresql/data  # Faster, ephemeral

Run tests:

1
docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm app

Useful Commands

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Start in background
docker compose up -d

# Rebuild and start
docker compose up -d --build

# View logs
docker compose logs -f app

# Execute command in running container
docker compose exec app sh

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

# Stop and remove
docker compose down

# Stop, remove, and delete volumes
docker compose down -v

# Pull latest images
docker compose pull

# View running services
docker compose ps

# View resource usage
docker compose top

Good Compose configurations are layered: base settings that work everywhere, with targeted overrides for each environment. Keep the base minimal, let overrides handle specifics.

Start simple, add complexity only when needed. A working two-service setup beats an elaborate configuration that nobody understands.