DockerDocker ComposeFull-StackMongoDB Intermediate 5 min read

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
Compose V2
Modern Docker installs ship with Compose V2 built in as docker compose (no hyphen). The older standalone docker-compose (with hyphen) is deprecated. Use docker compose in all new projects.

Core Compose Concepts

ConceptDescription
serviceOne container definition (image, ports, volumes…)
networkVirtual network services are connected to
volumeNamed persistent storage shared across services
depends_onStart order dependency between services
env_fileLoad 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
Never store secrets in docker-compose.yml
Always use a .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

CommandWhat it does
docker compose up -dStart all services in background
docker compose up -d --buildRebuild images then start
docker compose downStop + remove containers & networks
docker compose down -vAlso delete volumes
docker compose psService status
docker compose logs -fStream all logs
docker compose exec api shShell 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!