MeshWorld MeshWorld.
Cheatsheet Docker DevOps Backend Developer Tools 8 min read

Docker Compose Cheat Sheet: Services, Volumes, Networks & Production Patterns

Cobie
By Cobie
| Updated: Apr 9, 2026

Quick reference tables

Core CLI commands

CommandWhat it does
docker compose upCreate and start all services
docker compose up -dStart in detached (background) mode
docker compose up --buildRebuild images before starting
docker compose downStop and remove containers and networks
docker compose down -vAlso remove named volumes
docker compose down --rmi allAlso remove images
docker compose stopStop running containers (keep them)
docker compose startStart stopped containers
docker compose restartRestart all services
docker compose restart webRestart a specific service
docker compose pullPull latest images for all services
docker compose buildBuild all service images
docker compose build --no-cacheRebuild without cache

Service inspection

CommandWhat it does
docker compose psList containers and their status
docker compose ps -aInclude stopped containers
docker compose logsView all service logs
docker compose logs -fFollow (tail) logs in real time
docker compose logs -f webFollow logs for a specific service
docker compose logs --tail=100 webLast 100 lines from a service
docker compose topShow running processes in containers
docker compose eventsStream Docker events

Executing commands in containers

CommandWhat it does
docker compose exec web bashOpen a shell in a running container
docker compose exec web shUse sh if bash is not available
docker compose exec db psql -U user -d mydbRun command in running container
docker compose run --rm web python manage.py migrateRun a one-off command (new container)
docker compose run --rm web pytestRun tests in a fresh container
docker compose run --no-deps --rm web bashRun without starting linked services

Service definition fields

FieldWhat it does
image: postgres:16Use a pre-built image
build: .Build from Dockerfile in current directory
build: { context: ./app, dockerfile: Dockerfile.prod }Custom build context and file
container_name: myappSet a fixed container name
restart: alwaysAlways restart on failure or reboot
restart: unless-stoppedRestart unless manually stopped
restart: on-failureRestart 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

PatternWhat it does
environment: KEY=valueInline value
environment: KEYInherit from host shell
env_file: .envLoad all vars from a file
env_file: [.env, .env.local]Load multiple files (later overrides earlier)
--env-file .env.prodOverride env file from CLI

Volumes

PatternWhat 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: /tmpIn-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

PatternWhat it does
networks: [frontend, backend]Attach service to multiple networks
networks: { backend: { aliases: [db] } }Add a DNS alias on the network
driver: bridgeDefault isolated network (default)
driver: hostUse host network stack (Linux only)
external: trueUse a pre-existing Docker network

Top-level networks block:

networks:
  frontend:
  backend:
    internal: true  # No external internet access

Health checks

FieldWhat it does
test: ["CMD", "curl", "-f", "http://localhost"]HTTP health check
test: ["CMD-SHELL", "pg_isready -U user"]Shell command health check
interval: 30sCheck every 30 seconds
timeout: 10sFail check if no response in 10s
retries: 3Mark unhealthy after 3 consecutive failures
start_period: 40sGrace 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 backend only — 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_healthy means the API won’t start until Postgres is actually accepting connections
  • The internal: true backend 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)

  1. Values set with --env-file on the CLI
  2. Shell environment variables exported in the current session
  3. .env file in the project directory
  4. environment: block in docker-compose.yml
  5. Default values from the image ENV layer

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