Docker Compose turns a handful of docker run commands into a single docker compose up. This reference covers everything from the compose.yml structure to networking, volumes, health checks, and production-ready patterns.
Compose V2 note:
docker compose(space, no hyphen) is the current plugin. The olddocker-compose(hyphen) standalone binary is deprecated. Both use the same YAML format.
Quick reference
The 22 commands that cover 95% of daily Compose work.
| Command | What it does |
|---|---|
docker compose up |
Start all services (foreground) |
docker compose up -d |
Start all services (detached/background) |
docker compose up --build |
Rebuild images then start |
docker compose down |
Stop and remove containers + networks |
docker compose down -v |
Also remove named volumes |
docker compose stop |
Stop containers (keep them) |
docker compose start |
Start stopped containers |
docker compose restart |
Restart all services |
docker compose restart web |
Restart one service |
docker compose ps |
List running services |
docker compose logs |
View all logs |
docker compose logs -f web |
Follow logs for one service |
docker compose exec web bash |
Shell into running container |
docker compose run web npm test |
Run one-off command in new container |
docker compose build |
Build all images |
docker compose build web |
Build one image |
docker compose pull |
Pull latest images |
docker compose config |
Validate and print resolved config |
docker compose top |
Show running processes |
docker compose events |
Stream lifecycle events |
docker compose scale web=3 |
Scale a service (or use --scale) |
docker compose cp web:/app/log.txt . |
Copy file from container |
compose.yml anatomy
A complete example: web app + Postgres + Redis.
# compose.yml (or docker-compose.yml — both work)
name: myapp # optional project name
services:
web:
build:
context: . # Dockerfile location
dockerfile: Dockerfile # default; omit if using "Dockerfile"
args:
NODE_ENV: production
image: myapp-web:latest # tag the built image
ports:
- "3000:3000" # host:container
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/mydb
REDIS_URL: redis://cache:6379
env_file:
- .env # loaded on top of environment:
depends_on:
db:
condition: service_healthy # wait for health check
cache:
condition: service_started # just wait for start
volumes:
- ./src:/app/src # bind mount for dev hot-reload
- node_modules:/app/node_modules # named volume (don't overwrite)
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
networks:
- backend
command: redis-server --appendonly yes # override default CMD
volumes:
pgdata: # named volume, managed by Docker
redisdata:
node_modules:
networks:
backend:
driver: bridge # default; explicit is clearer
CLI commands in depth
Starting services
# Start everything in background
docker compose up -d
# Start specific services only
docker compose up -d web cache
# Build before starting (when Dockerfile changed)
docker compose up -d --build
# Remove orphan containers (services removed from compose.yml)
docker compose up -d --remove-orphans
# Force recreate all containers
docker compose up -d --force-recreate
Stopping and cleaning up
# Stop and remove containers + default network
docker compose down
# Also remove named volumes (deletes DB data!)
docker compose down -v
# Also remove built images
docker compose down --rmi all
# Just stop, keep containers for inspection
docker compose stop
# Remove stopped containers
docker compose rm -f
Logs and debugging
# Tail logs for all services
docker compose logs -f
# Tail logs for one service, last 100 lines
docker compose logs -f --tail=100 web
# Shell into running service
docker compose exec web bash
docker compose exec db psql -U postgres mydb
# Run a one-off command (new container, exits after)
docker compose run --rm web node scripts/migrate.js
# Show all running processes inside containers
docker compose top
Scaling
# Scale web service to 3 replicas
docker compose up -d --scale web=3
# Check all replicas
docker compose ps
Scaling works best with
portsusing only the container port (e.g.,- "3000") so Docker auto-assigns host ports. A load balancer (Nginx, Traefik) in front handles routing.
Environment variables
Three ways to pass env vars
services:
web:
# 1. Inline — visible in compose.yml (ok for non-secrets)
environment:
NODE_ENV: production
PORT: "3000"
# 2. From .env file (gitignored, per service)
env_file:
- .env
- .env.local # merged in order, later overrides earlier
# 3. Pass through from host shell (value from your terminal)
environment:
- API_KEY # no value = inherit from host
Variable substitution
Compose reads .env in the project root automatically for variable substitution:
# .env
POSTGRES_VERSION=16
APP_PORT=3000
# compose.yml
services:
db:
image: postgres:${POSTGRES_VERSION:-15} # fallback to 15 if unset
web:
ports:
- "${APP_PORT}:3000"
# Override at runtime
APP_PORT=8080 docker compose up -d
# Or with --env-file
docker compose --env-file .env.staging up -d
Volumes
Named volumes vs bind mounts
volumes:
# Named volume — Docker manages location, persists across down/up
pgdata:/var/lib/postgresql/data
# Bind mount — maps host path to container path
./src:/app/src # relative to compose.yml
# Read-only bind mount
./config/nginx.conf:/etc/nginx/nginx.conf:ro
# tmpfs — in-memory, lost on restart (fast, good for tests)
/tmp/cache:
type: tmpfs
tmpfs:
size: 100m
Exclude node_modules from bind mount
services:
web:
volumes:
- .:/app # bind mount entire project
- /app/node_modules # anonymous volume hides host's node_modules
Networks
Default behaviour
Compose creates one default network per project. Every service joins it and can reach other services by service name as hostname:
# Inside web container:
curl http://db:5432 # "db" resolves to the db service container
redis-cli -h cache # "cache" resolves to the redis container
Custom networks
services:
web:
networks:
- frontend
- backend
db:
networks:
- backend # db is not reachable from the "frontend" network
networks:
frontend:
backend:
internal: true # no external internet access
Host network (Linux only)
services:
web:
network_mode: host # container uses host's network stack directly
Health checks
Health checks let depends_on: condition: service_healthy work properly.
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s # how often to check
timeout: 5s # max time for one check
retries: 5 # fail after this many consecutive failures
start_period: 30s # grace period before failures count
web:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
Profiles
Profiles let you start subsets of services.
services:
web:
# no profile = always started
db:
# no profile = always started
mailhog:
profiles: [dev] # only starts when "dev" profile active
swagger-ui:
profiles: [dev, docs]
worker:
profiles: [worker]
# Start default services + dev profile
docker compose --profile dev up -d
# Multiple profiles
docker compose --profile dev --profile worker up -d
Production patterns
Pattern 1: override files for dev vs prod
# compose.yml — shared base config
# compose.override.yml — auto-merged in dev (bind mounts, ports)
# compose.prod.yml — production overrides
# Dev (auto-merges compose.yml + compose.override.yml)
docker compose up -d
# Production
docker compose -f compose.yml -f compose.prod.yml up -d
# compose.prod.yml
services:
web:
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
Pattern 2: wait-for-it with depends_on
For services without a proper health check, use depends_on: condition: service_started and a startup probe inside the app, or use a sidecar script:
services:
web:
depends_on:
db:
condition: service_healthy
command: ["./wait-for-it.sh", "db:5432", "--", "node", "server.js"]
Pattern 3: secrets (Docker Swarm / Compose v2)
services:
web:
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt # dev only
# external: true # prod: managed by Swarm/Kubernetes
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Using depends_on: without condition: service_healthy |
App starts before DB is ready — connection errors | Add health check to dependency, use condition: service_healthy |
Hardcoding secrets in compose.yml |
Secrets leak into version control | Use .env (gitignored) or Docker secrets |
Bind-mounting entire project over node_modules |
Container uses host's node_modules (wrong OS/arch) | Add anonymous volume for node_modules: - /app/node_modules |
Using version: top-level key |
Deprecated warning in Compose V2 | Remove version: — it's ignored but noisy |
docker compose down -v in production |
Deletes database volumes | Never run -v on production; remove named volumes manually |
Port conflicts with docker compose up |
Service fails to bind port | Use docker compose ps and docker ps -a to find conflicting container |
docker compose run without --rm |
Leaves stopped containers behind | Always use docker compose run --rm for one-off commands |
FAQ
docker compose up vs docker compose up --build — when to rebuild?up uses the cached image. up --build rebuilds images from Dockerfile. Rebuild when you change Dockerfile, package.json (for npm install layer), or any COPYed file that affects the image (not bind-mounted source).
How do services find each other by name?
Compose creates a DNS entry per service on the shared network. web can reach db at db:5432 because Docker's internal DNS resolves service names. This only works on the same Compose-managed network.
docker compose exec vs docker compose run?exec runs a command inside an already running container. run starts a new container from the service image, runs the command, then exits. Use exec for debugging running services; use run --rm for migrations or one-off scripts.
How do I view the final resolved config (variables substituted)?docker compose config prints the merged, fully-resolved YAML. Useful to verify .env substitution and override file merges before deploying.
Can I use Compose for production?
Single-server production: yes, with restart: unless-stopped or restart: always. For multi-host or high availability you need Docker Swarm (docker stack deploy) or Kubernetes. Compose lacks built-in load balancing, rolling updates, and auto-healing across hosts.
How do I pass --build-arg in Compose?
Use build.args in compose.yml:
build:
context: .
args:
NODE_VERSION: 20
BUILD_DATE: "2026-07-13"
Or override at CLI: docker compose build --build-arg NODE_VERSION=22 web.