04-Docker Compose - Multi-Container Apps
Use Docker Compose to define and run multi-container applications. Build a full-stack app with Node.js, MongoDB, Redis, and Nginx in a single YAML file.
What is Docker Compose?
Real applications are rarely a single container. A typical web app needs:
- An API server (Node.js / Python / Go)
- A database (PostgreSQL / MongoDB)
- A cache (Redis)
- A reverse proxy (Nginx)
Running each with separate docker run commands and manually wiring them together is painful. Docker Compose solves this with a single docker-compose.yml file.
docker compose up -d # Start everything
docker compose down # Stop and remove everything
docker compose (no hyphen). The older standalone docker-compose (with hyphen) is deprecated. Use docker compose in all new projects.Core Compose Concepts
| Concept | Description |
|---|---|
| service | One container definition (image, ports, volumes…) |
| network | Virtual network services are connected to |
| volume | Named persistent storage shared across services |
| depends_on | Start order dependency between services |
| env_file | Load environment variables from a .env file |
A Minimal Example
docker-compose.yml:
services:
web:
image: nginx:alpine
ports:
- "80:80"
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d
docker compose ps # check status
docker compose down # stop (keeps volumes)
docker compose down -v # stop + delete volumes
Full-Stack Project: Node.js + MongoDB + Redis + Nginx
Let’s build a production-style compose file for a real app.
Project structure:
my-fullstack-app/
├── docker-compose.yml
├── docker-compose.override.yml # dev overrides
├── .env
├── nginx/
│ └── nginx.conf
└── api/
├── Dockerfile
├── package.json
└── src/
└── index.js
.env (never commit this!):
NODE_ENV=production
API_PORT=3000
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=super_secret_password
MONGO_DB_NAME=myapp
REDIS_PASSWORD=redis_secret
docker-compose.yml:
name: myapp
services:
# ── Reverse Proxy ──────────────────────────
nginx:
image: nginx:1.25-alpine
container_name: myapp-nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
api:
condition: service_healthy
restart: unless-stopped
networks:
- frontend
# ── Node.js API ────────────────────────────
api:
build:
context: ./api
dockerfile: Dockerfile
target: production
container_name: myapp-api
env_file: .env
environment:
MONGO_URI: mongodb://admin:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017/${MONGO_DB_NAME}?authSource=admin
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
restart: unless-stopped
networks:
- frontend
- backend
# ── MongoDB ────────────────────────────────
mongo:
image: mongo:7.0
container_name: myapp-mongo
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: ${MONGO_DB_NAME}
volumes:
- mongo_data:/data/db
- ./mongo/init.js:/docker-entrypoint-initdb.d/init.js:ro
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
networks:
- backend
# ── Redis ──────────────────────────────────
redis:
image: redis:7.2-alpine
container_name: myapp-redis
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- backend
volumes:
mongo_data:
driver: local
redis_data:
driver: local
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # ← MongoDB and Redis NOT accessible from outside host
nginx/nginx.conf:
events { worker_connections 1024; }
http {
upstream api {
server api:3000;
}
server {
listen 80;
location / {
proxy_pass http://api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
}
Development Override
Use docker-compose.override.yml to add hot-reload and debug tools in dev without touching the main file:
# docker-compose.override.yml (auto-loaded by docker compose)
services:
api:
build:
target: development # uses dev stage from multi-stage Dockerfile
volumes:
- ./api/src:/app/src:ro # bind mount for hot reload
environment:
NODE_ENV: development
command: ["node", "--watch", "src/index.js"]
mongo:
ports:
- "27017:27017" # expose mongo to host in dev only
redis:
ports:
- "6379:6379" # expose redis to host in dev only
Useful Compose Commands
# Start services (build if needed)
docker compose up -d
# Start and force rebuild images
docker compose up -d --build
# Scale a service to 3 replicas
docker compose up -d --scale api=3
# View running services and ports
docker compose ps
# Stream logs from all services
docker compose logs -f
# Logs from specific service only
docker compose logs -f api
# Execute command in a service container
docker compose exec mongo mongosh -u admin -p
# Run a one-off command in a new container
docker compose run --rm api npm run migrate
# Pull latest images
docker compose pull
# Stop services (keep containers + volumes)
docker compose stop
# Stop and remove containers + networks
docker compose down
# Stop and remove EVERYTHING including volumes
docker compose down -v --rmi all
.env file (and add it to .gitignore) for passwords and API keys. Or better yet, use Docker Secrets or a secret manager like AWS Secrets Manager in production.Healthchecks Explained
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s # check every 30 seconds
timeout: 10s # fail if takes longer than 10s
retries: 3 # mark unhealthy after 3 consecutive failures
start_period: 15s # grace period on startup before counting failures
depends_on: condition: service_healthy makes Docker wait for a container to pass its healthcheck before starting dependent services — essential for reliable startup ordering.
Summary
| Command | What it does |
|---|---|
docker compose up -d | Start all services in background |
docker compose up -d --build | Rebuild images then start |
docker compose down | Stop + remove containers & networks |
docker compose down -v | Also delete volumes |
docker compose ps | Service status |
docker compose logs -f | Stream all logs |
docker compose exec api sh | Shell into running service |
You now know how to run multi-container applications with Docker Compose. Next, we’ll look at Docker Volumes and Storage in depth!