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
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"
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
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
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
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:
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
And the backend container can communicate with the database because both share the same network
backend:
networks:
- backend-network
postgres:
networks:
- backend-network
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:
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
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
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
Here we use nginx as our reverse proxy, the nginx container is the only service exposed to the host:
ports:
- "80:80"
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;
}
}
}
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
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
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)