Quick reference tables
Core CLI commands
| Command | What it does |
|---|---|
docker compose up | Create and start all services |
docker compose up -d | Start in detached (background) mode |
docker compose up --build | Rebuild images before starting |
docker compose down | Stop and remove containers and networks |
docker compose down -v | Also remove named volumes |
docker compose down --rmi all | Also remove images |
docker compose stop | Stop running containers (keep them) |
docker compose start | Start stopped containers |
docker compose restart | Restart all services |
docker compose restart web | Restart a specific service |
docker compose pull | Pull latest images for all services |
docker compose build | Build all service images |
docker compose build --no-cache | Rebuild without cache |
Service inspection
| Command | What it does |
|---|---|
docker compose ps | List containers and their status |
docker compose ps -a | Include stopped containers |
docker compose logs | View all service logs |
docker compose logs -f | Follow (tail) logs in real time |
docker compose logs -f web | Follow logs for a specific service |
docker compose logs --tail=100 web | Last 100 lines from a service |
docker compose top | Show running processes in containers |
docker compose events | Stream Docker events |
Executing commands in containers
| Command | What it does |
|---|---|
docker compose exec web bash | Open a shell in a running container |
docker compose exec web sh | Use sh if bash is not available |
docker compose exec db psql -U user -d mydb | Run command in running container |
docker compose run --rm web python manage.py migrate | Run a one-off command (new container) |
docker compose run --rm web pytest | Run tests in a fresh container |
docker compose run --no-deps --rm web bash | Run without starting linked services |
Service definition fields
| Field | What it does |
|---|---|
image: postgres:16 | Use a pre-built image |
build: . | Build from Dockerfile in current directory |
build: { context: ./app, dockerfile: Dockerfile.prod } | Custom build context and file |
container_name: myapp | Set a fixed container name |
restart: always | Always restart on failure or reboot |
restart: unless-stopped | Restart unless manually stopped |
restart: on-failure | Restart only on non-zero exit |
ports: ["3000:3000"] | Map host port to container port |
expose: ["3000"] | Expose port to other services (not host) |
depends_on: [db] | Start after dependency is running |
depends_on: { db: { condition: service_healthy } } | Wait for health check to pass |
profiles: [dev] | Only start when this profile is active |
Environment variables
| Pattern | What it does |
|---|---|
environment: KEY=value | Inline value |
environment: KEY | Inherit from host shell |
env_file: .env | Load all vars from a file |
env_file: [.env, .env.local] | Load multiple files (later overrides earlier) |
--env-file .env.prod | Override env file from CLI |
Volumes
| Pattern | What it does |
|---|---|
volumes: [./data:/var/lib/postgresql/data] | Bind mount (host path) |
volumes: [pgdata:/var/lib/postgresql/data] | Named volume (managed by Docker) |
volumes: [./app:/app:ro] | Read-only bind mount |
volumes: [./app:/app:cached] | macOS performance hint |
tmpfs: /tmp | In-memory volume (not persisted) |
Top-level volumes block (required for named volumes):
volumes:
pgdata: # Docker-managed named volume
uploads:
driver: local # Explicit driver (default)
Networks
| Pattern | What it does |
|---|---|
networks: [frontend, backend] | Attach service to multiple networks |
networks: { backend: { aliases: [db] } } | Add a DNS alias on the network |
driver: bridge | Default isolated network (default) |
driver: host | Use host network stack (Linux only) |
external: true | Use a pre-existing Docker network |
Top-level networks block:
networks:
frontend:
backend:
internal: true # No external internet access
Health checks
| Field | What it does |
|---|---|
test: ["CMD", "curl", "-f", "http://localhost"] | HTTP health check |
test: ["CMD-SHELL", "pg_isready -U user"] | Shell command health check |
interval: 30s | Check every 30 seconds |
timeout: 10s | Fail check if no response in 10s |
retries: 3 | Mark unhealthy after 3 consecutive failures |
start_period: 40s | Grace period before checks begin |
Detailed sections
Minimal production compose file
A realistic three-tier app: Node.js API, PostgreSQL database, Nginx reverse proxy. Replace values as needed.
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
api:
build:
context: ./api
dockerfile: Dockerfile
restart: unless-stopped
environment:
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
NODE_ENV: production
depends_on:
db:
condition: service_healthy
networks:
- backend
- frontend
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/ssl:ro
depends_on:
- api
networks:
- frontend
volumes:
pgdata:
networks:
frontend:
backend:
internal: true
Key points in this config:
- The database is on
backendonly — it’s not reachable from the host or from Nginx directly - The API is on both networks — it can reach the database and be reached by Nginx
service_healthymeans the API won’t start until Postgres is actually accepting connections- The
internal: truebackend network has no internet routing — containers on it cannot initiate outbound connections
Override files for dev vs. production
The standard pattern: docker-compose.yml is the base, docker-compose.override.yml adds dev-specific config that is automatically merged.
# docker-compose.override.yml (dev only, NOT committed if it has secrets)
services:
api:
build:
target: development # Multi-stage: use dev stage
volumes:
- ./api:/app # Live code reload
environment:
NODE_ENV: development
DEBUG: "app:*"
ports:
- "9229:9229" # Node.js debugger port
db:
ports:
- "5432:5432" # Expose DB port locally for psql/DBeaver
For production, pass the prod override explicitly:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
The -f flag stops the automatic override merge, so only the files you list are used.
Environment variable precedence (highest to lowest)
- Values set with
--env-fileon the CLI - Shell environment variables exported in the current session
.envfile in the project directoryenvironment:block indocker-compose.yml- Default values from the image
ENVlayer
Practical consequence: if NODE_ENV=production is in your shell, it overrides whatever is in the .env file. This is often surprising on CI servers where the shell environment already has values set by the CI platform.
Multi-stage builds with compose
For APIs or apps that have both a dev and prod image from the same Dockerfile:
# Dockerfile
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS development
RUN npm install --include=dev
CMD ["npm", "run", "dev"]
FROM base AS production
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
# docker-compose.yml (production target)
services:
api:
build:
context: .
target: production
# docker-compose.override.yml (development target, no target = dev)
services:
api:
build:
target: development
volumes:
- .:/app
- /app/node_modules # Anonymous volume so host node_modules don't override
The anonymous volume trick (/app/node_modules) is important: without it, the bind mount would overwrite the container’s node_modules with whatever (or nothing) is in your host directory.
Profiles — optional services
Profiles let you define services that only start when you specifically request them. Common uses: development databases, mock servers, test utilities, admin UIs.
services:
api:
build: .
# No profile — always starts
db:
image: postgres:16-alpine
# No profile — always starts
adminer:
image: adminer
ports:
- "8080:8080"
profiles:
- dev # Only starts with: docker compose --profile dev up
redis:
image: redis:alpine
profiles:
- dev
- cache
# Start base services only
docker compose up -d
# Start base + dev profile services
docker compose --profile dev up -d
# Start base + both profiles
docker compose --profile dev --profile cache up -d
Scaling services
# Run 3 replicas of the worker service
docker compose up -d --scale worker=3
For this to work cleanly, the scaled service must not use container_name (would conflict) and must not bind a host port directly. Use a load balancer (Nginx, Traefik) in front of it instead.
Cleaning up
# Remove stopped containers
docker compose rm
# Remove containers and volumes (database data included)
docker compose down -v
# Remove all unused containers, networks, images (not just compose)
docker system prune -f
# Nuclear option: also remove volumes
docker system prune -f --volumes
Related: Docker Cheat Sheet | Nginx Cheat Sheet | How to Write a Node.js Dockerfile