Docker Compose is the tool for defining and running multi-container Docker applications. With a single docker-compose.yml file you describe the complete MEAN Stack — Express API, Angular dev server, MongoDB, Redis for caching and sessions, and an nginx reverse proxy — and start everything with one command: docker compose up. Compose handles container creation, network setup, volume mounting, environment variables, dependency ordering, and health check integration automatically.
Docker Compose Key Fields
| Field | Purpose | Example |
|---|---|---|
services |
Define each container/service | services: api: ... |
image |
Use a pre-built image | image: mongo:7 |
build |
Build from a Dockerfile | build: { context: ./api, target: development } |
ports |
Map host:container ports | ports: ["3000:3000"] |
volumes |
Mount volumes or host paths | volumes: ["./api:/app", "node_modules:/app/node_modules"] |
environment |
Set environment variables | environment: { NODE_ENV: development } |
env_file |
Load env vars from a file | env_file: ./api/.env |
depends_on |
Start order + health dependency | depends_on: { mongodb: { condition: service_healthy } } |
networks |
Connect to named networks | networks: [backend, frontend] |
healthcheck |
Test if service is ready | test: ["CMD", "mongosh", "--eval", "db.runCommand({ping:1})"] |
restart |
Restart policy | restart: unless-stopped |
mongodb://mongodb:27017/taskmanager — where mongodb is the service name defined in docker-compose.yml. Docker’s built-in DNS resolves service names to container IP addresses automatically. You never need to hard-code IP addresses between services.node_modules inside the container when bind-mounting source code for hot reload. The pattern volumes: ["./api:/app", "node_modules:/app/node_modules"] mounts the host source directory into /app but keeps the container’s node_modules separate (in a named volume). Without this, the host’s node_modules (built for the host OS) would overwrite the container’s node_modules (built for Linux) — causing native module errors on macOS and Windows.depends_on alone does not guarantee a service is ready before the dependent service starts — only that the container has started. Use depends_on: { mongodb: { condition: service_healthy } } with a healthcheck on the MongoDB service to ensure the Express API only starts after MongoDB is accepting connections. Without this, the API may crash on startup with “connection refused” because MongoDB is still initialising.Complete Docker Compose Configuration
# docker-compose.yml — Development environment
version: '3.9'
services:
# ── MongoDB ──────────────────────────────────────────────────────────────
mongodb:
image: mongo:7
container_name: taskmanager_mongo
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-admin}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASS:-secret}
MONGO_INITDB_DATABASE: taskmanager
volumes:
- mongodb_data:/data/db # persist data across restarts
- ./mongo-init.js:/docker-entrypoint-initdb.d/init.js:ro # seed script
ports:
- "27017:27017" # expose for MongoDB Compass (dev only)
networks:
- backend
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.runCommand({ping:1})", "--quiet"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# ── Redis ─────────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
container_name: taskmanager_redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD:-redispass}
volumes:
- redis_data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redispass}", "ping"]
interval: 10s
timeout: 5s
retries: 3
# ── Express API ───────────────────────────────────────────────────────────
api:
build:
context: ./api
target: development # use the dev stage with nodemon
container_name: taskmanager_api
restart: unless-stopped
env_file: ./api/.env
environment:
NODE_ENV: development
MONGO_URI: mongodb://${MONGO_ROOT_USER:-admin}:${MONGO_ROOT_PASS:-secret}@mongodb:27017/taskmanager?authSource=admin
REDIS_URL: redis://:${REDIS_PASSWORD:-redispass}@redis:6379
volumes:
- ./api:/app # bind mount source for hot reload
- api_modules:/app/node_modules # named volume for container's node_modules
ports:
- "3000:3000"
networks:
- backend
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
# ── Angular Dev Server ────────────────────────────────────────────────────
angular:
build:
context: ./client
target: development
container_name: taskmanager_angular
restart: unless-stopped
environment:
- CHOKIDAR_USEPOLLING=true # required for file watching inside Docker on macOS/Windows
volumes:
- ./client/src:/app/src # hot reload for source changes
- ng_modules:/app/node_modules
ports:
- "4200:4200"
networks:
- frontend
depends_on:
- api
# ── nginx reverse proxy ───────────────────────────────────────────────────
nginx:
image: nginx:alpine
container_name: taskmanager_nginx
restart: unless-stopped
volumes:
- ./nginx/dev.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "80:80"
networks:
- frontend
- backend
depends_on:
- api
- angular
# ── Volumes ───────────────────────────────────────────────────────────────
volumes:
mongodb_data:
redis_data:
api_modules:
ng_modules:
# ── Networks ──────────────────────────────────────────────────────────────
networks:
backend:
driver: bridge
frontend:
driver: bridge
# Common Docker Compose commands
docker compose up # start all services, attach logs
docker compose up -d # start detached (background)
docker compose up --build # rebuild images before starting
docker compose down # stop and remove containers/networks
docker compose down -v # also remove volumes (wipe data)
docker compose logs -f api # follow logs for the api service
docker compose exec api sh # open shell in running api container
docker compose ps # list service status
docker compose restart api # restart one service
docker compose build api # rebuild one service's image
# Override for production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
How It Works
Step 1 — Compose Creates an Isolated Network for All Services
When you run docker compose up, Compose creates a virtual network named projectname_default (plus any explicitly named networks in the file). Every service joins this network and gets a DNS entry matching its service name. The api service resolves mongodb to the MongoDB container’s IP, redis to the Redis container’s IP — no configuration needed beyond the service name.
Step 2 — depends_on with Healthcheck Ensures Correct Startup Order
depends_on: { mongodb: { condition: service_healthy } } makes the api service wait until MongoDB’s healthcheck passes before starting. The healthcheck runs mongosh --eval "db.runCommand({ping:1})" every 10 seconds. Once MongoDB responds successfully, the api container starts. This eliminates the race condition where Node.js tries to connect to MongoDB before it is ready to accept connections.
Step 3 — Named Volumes for node_modules Solve Cross-Platform Issues
When the host source directory is bind-mounted into the container, the host’s node_modules would also be visible inside the container — overriding the container’s clean install. The api_modules:/app/node_modules named volume mounts a separate, container-internal volume at /app/node_modules, shadowing the bind-mounted version. The container always uses its own node_modules, built for the Linux container OS.
Step 4 — CHOKIDAR_USEPOLLING Enables File Watching in Containers
File watching utilities (used by nodemon and the Angular CLI) typically use inotify (Linux file system events). When the source directory is bind-mounted from a macOS or Windows host, these events do not propagate into the container reliably. Setting CHOKIDAR_USEPOLLING=true switches to polling — checking file modification times periodically — which works across all host operating systems at the cost of slightly higher CPU usage.
Step 5 — Compose Overrides Enable Environment-Specific Configuration
docker compose -f docker-compose.yml -f docker-compose.prod.yml up merges two Compose files. The production override file only needs to specify what differs from the base file — different image tags, removed debug ports, added resource limits, changed restart policies. This avoids duplicating the entire configuration for each environment while keeping environment differences explicit and reviewable.
Common Mistakes
Mistake 1 — Using depends_on without healthcheck condition
❌ Wrong — api starts immediately after mongodb container starts (not when ready):
api:
depends_on:
- mongodb # container started, but MongoDB may not be accepting connections!
✅ Correct — wait for healthy status:
api:
depends_on:
mongodb:
condition: service_healthy # waits for healthcheck to pass
Mistake 2 — Exposing MongoDB port in production
❌ Wrong — MongoDB is accessible from the internet in production:
mongodb:
ports:
- "27017:27017" # in production — accessible externally!
✅ Correct — no ports exposure for internal services:
mongodb:
# No ports — only accessible within the Docker network
# api reaches it via: mongodb://mongodb:27017
Mistake 3 — Not using a named volume for node_modules
❌ Wrong — host’s macOS node_modules overwrite container’s Linux node_modules:
volumes:
- ./api:/app # host node_modules visible as /app/node_modules — wrong architecture!
✅ Correct — shadow with a named volume:
volumes:
- ./api:/app
- api_modules:/app/node_modules # shadows the bind mount at this path
Quick Reference
| Task | Command |
|---|---|
| Start all services | docker compose up -d |
| Rebuild and start | docker compose up -d --build |
| Stop all services | docker compose down |
| View logs | docker compose logs -f servicename |
| Shell into service | docker compose exec api sh |
| Run one-off command | docker compose run --rm api npm test |
| Wipe volumes | docker compose down -v |
| Service DNS | Service name resolves as hostname: mongodb://mongodb:27017 |
| Named volume for node_modules | volumes: ["./src:/app", "node_mods:/app/node_modules"] |