Hands-on tutorial for running multi-container applications. Follow it alongside this lesson. docs.docker.com →
Real apps don't run in isolation. Your Node.js API needs a database, Redis for caching, maybe RabbitMQ for queues. Running each container with docker run manually — with all the right flags, networks, and volumes — is a nightmare. Compose defines your entire multi-container stack in one YAML file.
docker-compose.yml is like a package.json for your infrastructure. Instead of npm install, you run docker compose up to spin up the entire stack.
This runs a Node.js API with PostgreSQL, Redis, and RabbitMQ:
version: "3.9"
services:
# ── Your Application ──────────────────────────────────────────
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:password@db:5432/myapp
REDIS_URL: redis://redis:6379
RABBITMQ_URL: amqp://user:password@rabbitmq:5672
env_file:
- .env # Load additional secrets from .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
restart: unless-stopped
networks:
- app-network
# ── Background Worker ─────────────────────────────────────────
worker:
build: .
command: node workers/email-worker.js
environment:
NODE_ENV: production
RABBITMQ_URL: amqp://user:password@rabbitmq:5672
depends_on:
rabbitmq:
condition: service_healthy
restart: unless-stopped
networks:
- app-network
# ── PostgreSQL ────────────────────────────────────────────────
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# ── Redis ─────────────────────────────────────────────────────
redis:
image: redis:7-alpine
command: redis-server --appendonly yes # Enable AOF persistence
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- app-network
# ── RabbitMQ ──────────────────────────────────────────────────
rabbitmq:
image: rabbitmq:3-management-alpine
environment:
RABBITMQ_DEFAULT_USER: user
RABBITMQ_DEFAULT_PASS: password
ports:
- "15672:15672" # Management UI: localhost:15672
volumes:
- rabbitmq-data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 15s
timeout: 10s
retries: 5
networks:
- app-network
# ── Named volumes (persist data across restarts) ───────────────
volumes:
postgres-data:
redis-data:
rabbitmq-data:
# ── Internal network (containers talk to each other by name) ──
networks:
app-network:
driver: bridge
Inside the Docker network, containers find each other by service name. Your API connects to redis://redis:6379 — not localhost. The service name is the hostname.
Containers are ephemeral. Stop them, their filesystem is gone. Named volumes persist data independently of the container lifecycle. Always use volumes for databases.
depends_on with condition: service_healthy means "don't start my API until the database is actually ready to accept connections" — not just "until the container is started." This prevents race conditions on startup.
Automatically restarts the container if it crashes. Stops only if you explicitly docker compose down it.
# Start all services (detached)
docker compose up -d
# Start with rebuild (use after code changes)
docker compose up -d --build
# View logs (all services)
docker compose logs -f
# View logs for one service
docker compose logs -f api
# Check status
docker compose ps
# Run a one-off command
docker compose exec api sh
docker compose exec db psql -U user myapp
# Stop everything (keep volumes)
docker compose down
# Stop and delete all volumes (DESTROYS DATA)
docker compose down -v
# Scale a service
docker compose up -d --scale worker=3
Use separate Compose files for development and production. In dev, you want hot-reload and no build step:
# docker-compose.dev.yml — overrides for local dev
version: "3.9"
services:
api:
build:
target: dev # Use a dev stage in your Dockerfile
volumes:
- .:/app # Mount source code for hot-reload
- /app/node_modules
command: npx nodemon server.js
# Run dev stack
docker compose -f docker-compose.yml -f docker-compose.dev.yml up