How to Build Multi-Container Docker Compose Stacks (Production Architecture)
Complete DevOps guide to writing production-grade docker-compose.yml stacks with Node.js, PostgreSQL 16, Redis 7, Nginx reverse proxy, healthchecks, and internal bridge networks.
🛠️ Interactive Tool Available:
Want to generate a custom production-ready Docker Compose file visually without writing YAML from scratch? Use our Docker Compose Generator.
1. Core Principles of Multi-Container Architecture
Modern cloud applications rarely run as single monolithic binaries. A robust production stack typically decouples application logic from stateful databases, ephemeral caches, message brokers, and public ingress proxies.
When organizing services with Docker Compose, adhere to the three foundational pillars of container engineering:
- Single Responsibility: Each container must execute exactly one process (e.g. your Node.js API in container A, PostgreSQL in container B, and Redis in container C). Never install databases inside your application runtime image.
- Stateless Compute & Stateful Volumes: Containers must be treated as disposable cattle, not pets. All persistent data (database tables, uploads, log archives) must live on dedicated Docker Named Volumes.
- Network Isolation & Least Privilege: Only the reverse proxy (Nginx / Traefik / Caddy) should expose public ports (80/443). Your database and cache must remain strictly private within internal Docker bridge networks.
2. The Production-Ready 3-Tier Compose Template
Below is a full-featured, battle-tested docker-compose.yml file implementing healthchecks, isolated private networking, automatic restart policies, and named persistent storage.
version: '3.8'
services:
# -------------------------------------------------------------
# 1. Public Ingress / Reverse Proxy Layer
# -------------------------------------------------------------
reverse-proxy:
image: nginx:alpine
container_name: production_proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/ssl/certs:ro
depends_on:
api-server:
condition: service_healthy
networks:
- public-network
- private-backend
# -------------------------------------------------------------
# 2. Application Backend (Node.js / Express / Fastify)
# -------------------------------------------------------------
api-server:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: production_api
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=postgres://app_user:${POSTGRES_PASSWORD}@postgres-db:5432/production_db
- REDIS_URL=redis://redis-cache:6379
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
depends_on:
postgres-db:
condition: service_healthy
redis-cache:
condition: service_healthy
networks:
- private-backend
# -------------------------------------------------------------
# 3. Database Layer (PostgreSQL 16)
# -------------------------------------------------------------
postgres-db:
image: postgres:16-alpine
container_name: production_postgres
restart: unless-stopped
environment:
POSTGRES_DB: production_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d production_db"]
interval: 10s
timeout: 5s
retries: 5
networks:
- private-backend
# -------------------------------------------------------------
# 4. In-Memory Cache & Message Broker (Redis 7)
# -------------------------------------------------------------
redis-cache:
image: redis:7-alpine
container_name: production_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
- private-backend
# ---------------------------------------------------------------
# Persistent Storage Volumes
# ---------------------------------------------------------------
volumes:
postgres_data:
driver: local
redis_data:
driver: local
# ---------------------------------------------------------------
# Isolated Bridge Networks
# ---------------------------------------------------------------
networks:
public-network:
driver: bridge
private-backend:
driver: bridge
internal: true
3. Advanced Healthcheck Orchestration (Avoid Race Conditions)
One of the most frequent mistakes in Docker Compose is relying on basic depends_on: [postgres-db]. By default, Docker only checks if the container has started, not if PostgreSQL is ready to accept TCP socket connections.
To eliminate startup race conditions permanently, define explicit healthchecks using pg_isready and bind your dependent service using condition: service_healthy.
4. Security & Environment Variable Isolation
Never commit database passwords or JWT signing keys into your docker-compose.yml file. Always declare variables inside a separate .env file placed in your root directory, and add .env to your .gitignore.