DEV Community

Mo Rizal
Mo Rizal

Posted on

7 Docker Compose Patterns Every Devops Engineer Should Know

Docker compose has become one of the most widely used tools for running multi container applications, with a simple YAML file, engineers can spin up multiple containers at once

However, defining the right setup in your compose file is critical to ensure that different containers communicate, how data is persisted, and how the system operates efficiently

This is where docker compose patterns become valuable

In this article, we will explore 7 docker compose patterns that you should know

1. Multi Environment Pattern

In a real world project, development and production environment usually have different requirements

In development, engineers need fast iteration, the ability to rebuild images, and exposed ports for easier access. While production requires stable image, automatic restart and minimal configuration changes.

The Multi Environment Pattern solves this problem by separating common service definitions from environment specific configurations

Instead of maintaining multiple completely different compose files, we create a base docker-compose.yml and extend it with environment specific overrides.

  • docker-compose.yml → base
  • docker-compose.dev.yml → development
  • docker-compose.prod.yml → production

Base Setup

services:
  backend:
    working_dir: /app
    environment:
      NODE_ENV: ${NODE_ENV}
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

  redis:
    image: redis:8-alpine
Enter fullscreen mode Exit fullscreen mode

This file defines the application architecture without including any environment specific configuration

Development Setup

services:
  backend:
    build: .
    command: npm run dev
    volumes:
      - ./backend:/app
    ports:
      - "3000:3000"

  postgres:
    ports:
      - "5432:5432"

  redis:
    ports:
      - "6379:6379"
Enter fullscreen mode Exit fullscreen mode

In this file we add source code mounting and accessible ports

Production Setup

services:
  backend:
    image: ghcr.io/multienv/backend:latest
    command: npm start
    restart: unless-stopped

  postgres:
    restart: unless-stopped

  redis:
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Production should use pre-built images and automatic restart

2. Health Check & Dependency Pattern

In a multi container applications, service startup order does not ensure service readiness

For example, if you have appication with backend container and database container, docker may start the backend immediately after starting the database. However, the database might still be initializing and unable to accept connections thus causing startup failure

Health Check & Dependency Pattern solves this problem by allowing containers to verify service availability before starting dependent services

Example

services:
  backend:
    image: node:22-alpine
    working_dir: /app
    volumes:
      - ./backend:/app
    command: node index.js
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "pg_isready -U postgres -d app"
        ]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 20s
Enter fullscreen mode Exit fullscreen mode

The postgres container includes a health check using pg_isready:
pg_isready -U postgres -d app

The backend service uses:

depends_on:
      postgres:
        condition: service_healthy
Enter fullscreen mode Exit fullscreen mode

This tell docker to start the backend only after PostgreSQL reports a healthy status. With this setup we ensure that the right container start at the right time

3. Network Isolation Pattern

For security reasons, not every container should be able to communicate with each other

For example, a database should not be directly accessible from the internet. Only the appropriate services should be allowed to communicate with it

The Network Isolation Pattern improves security by separating containers into different docker networks and controlling container communication

Example

services:
  frontend:
    image: nginx:alpine
    ports:
      - "8080:80"
    networks:
      - frontend-network

  backend:
    image: node:22-alpine
    networks:
      - frontend-network
      - backend-network

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    networks:
      - backend-network

networks:
  frontend-network:
  backend-network:
Enter fullscreen mode Exit fullscreen mode

This setup creates two isolated networks:

  • frontend-network
  • backend-network

The frontend container can communicate with the backend because both services share the same network:

frontend:
    networks:
      - frontend-network
backend:
    networks:
      - frontend-network
Enter fullscreen mode Exit fullscreen mode

And the backend container can communicate with the database because both share the same network

backend:
    networks:
      - backend-network

  postgres:
    networks:
      - backend-network
Enter fullscreen mode Exit fullscreen mode

However, the frontend can't be connected to the database because the database container does not include the frontend-network

4. Persistent Volume Pattern

By default containers are not persistent, this means that when a container is removed, all data inside the container filesystem is deleted

This becomes a problem for stateful services such as databases, where data must survive container restarts, updates, or redeployments

The Persistent Volume Pattern solves this problem by storing important data outside the container lifecycle using Docker volumes

Example

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:
Enter fullscreen mode Exit fullscreen mode

The PostgreSQL container stores its database files in:
/var/lib/postgresql/data

Instead of storing this data inside the container, in this setup docker will mount the data in

volumes:
  - postgres-data:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode

If the container is recreated:
docker compose up -d

Docker attaches the existing volume and postgres can continue using the existing data

5. Resource Limit Pattern

Without proper resource limitation, a single container might consume excessive cpu or memory that can affect other services running on the host

The Resource Limit Pattern prevents this by defining proper limits for containers.

Example

services:
  backend:
    image: node:22-alpine
    working_dir: /app
    volumes:
      - ./backend:/app
    command: node index.js
    mem_limit: 512m
    mem_reservation: 256m
    cpus: 1
Enter fullscreen mode Exit fullscreen mode

In this example we define several config:

  • mem_limit: 512m - Defines the maximum memory a container can use
  • mem_reservation: 256m - Defines the minimum amount of memory that docker should reserve for the container
  • cpus: 1 - Limit the container to use one cpu core

6. Reverse Proxy Gateway Pattern

In production environment, applications are rarely exposed directly to the internet. Instead, traffic is handled by a reverse proxy that acts as a gateway between external users and internal services

The Reverse Proxy Pattern places a proxy server in front of application containers to handle incoming requests, routing, and load balancing

Example

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - backend

  backend:
    image: node:22-alpine
    working_dir: /app
    volumes:
      - ./backend:/app
    command: node index.js
Enter fullscreen mode Exit fullscreen mode

Here we use nginx as our reverse proxy, the nginx container is the only service exposed to the host:

ports:
  - "80:80"
Enter fullscreen mode Exit fullscreen mode

The backend service does not expose any ports because it only needs to communicate internally with nginx.

nginx.conf

events {}

http {
    server {
        listen 80;

        location / {
            proxy_pass http://backend:3000;

            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When a user accesses port 80, the request first reaches nginx which forwards the request internally to:
http://backend:3000

7. Secrets Management Pattern

Applications often require sensitive information such as database passwords, API keys, and authentication tokens

A common mistake is storing these credentials directly inside docker-compose.yml:

environment:
  POSTGRES_PASSWORD: password
Enter fullscreen mode Exit fullscreen mode

This creates security risks because secrets can accidentally be exposed through source code repositories, logs, or shared configuration files

The Secrets Management Pattern separates sensitive data from application configuration by using Docker secrets

Example

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password

secrets:
  postgres_password:
    file: ./postgres_password.txt
Enter fullscreen mode Exit fullscreen mode

Instead of passing the password directly:
POSTGRES_PASSWORD: my-secure-password

Docker mounts the secret as a file inside the container:
/run/secrets/postgres_password

The PostgreSQL image automatically reads the password from this file:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password

Docker secrets are useful for small deployments and single-host environments. However, for production systems it is recommended to use a dedicated secret management platform such as hashicorp vault, openbao, or Infisical

Conclusion

Docker compose patterns help devops engineers build container environments that are more secure, reliable, and easier to maintain

By applying these 7 patterns, you can create a more production ready setup

You can find the source code for this article in my github repository:
https://github.com/muhammadyulasfipahrizal/compose-patterns

Top comments (0)