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 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
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to