Docker Compose cheatsheet
Docker Compose describes a multi-container application in one YAML file and runs it with a single command. The file declares services (containers), plus the networks, volumes, secrets and configs they use.
âšī¸ The
version:key at the top is obsolete â the Compose Spec ignores it. Start the file withservices:.
đ File Names & Precedence
| File | Role |
|---|---|
compose.yaml | The preferred name (compose.yml also works) |
docker-compose.yaml | Legacy name, still supported |
compose.override.yaml | Merged on top of the base file automatically |
-f a.yaml -f b.yaml | Explicit list; later files override earlier ones |
.env | Variables for interpolation, read from the project directory |
# check what Compose actually resolved, after merges and interpolation
docker compose config
# use a specific set of files
docker compose -f compose.yaml -f compose.prod.yaml up -d
YAML rules that bite
- Two spaces per indent level, never tabs
- Keys and values are case-sensitive
- Lists use
-; quote any string containing:,#,{,}or a leading* yes/no/on/offare booleans â quote them if you mean the words
đ§ą Minimal Structure
services:
web:
image: nginx:1.27
ports:
- '8080:80'
depends_on:
- api
api:
build: .
environment:
DATABASE_URL: postgres://db:5432/app
networks:
- backend
networks:
backend:
volumes:
db-data:
âī¸ Service Options
services:
app:
# where the image comes from
image: myapp:1.2
build: .
pull_policy: always
# identity and lifecycle
container_name: myapp
hostname: app
restart: unless-stopped
init: true
stop_grace_period: 30s
# what it runs
entrypoint: ['/entrypoint.sh']
command: ['node', 'server.js']
working_dir: /app
user: '1000:1000'
# configuration
environment:
NODE_ENV: production
API_KEY: ${API_KEY}
env_file:
- .env
- .env.production
# connectivity
ports:
- '8080:80'
expose:
- '9000'
networks:
- frontend
extra_hosts:
- 'host.docker.internal:host-gateway'
dns:
- 1.1.1.1
# storage
volumes:
- db-data:/var/lib/data
- ./src:/app/src:ro
tmpfs:
- /tmp
# ordering and health
depends_on:
db:
condition: service_healthy
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost/health']
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
# host access and limits
cap_add:
- SYS_PTRACE
devices:
- /dev/dri:/dev/dri
ulimits:
nofile: 65535
deploy:
resources:
limits:
cpus: '1.5'
memory: 512M
# bookkeeping
labels:
com.example.team: platform
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'
đ Build Options
services:
app:
build:
context: .
dockerfile: docker/Dockerfile
target: production
args:
NODE_VERSION: '22'
cache_from:
- myapp:cache
secrets:
- npmrc
platforms:
- linux/amd64
- linux/arm64
image: myapp:1.2 # the name given to the built image
đĻ Volumes
services:
db:
volumes:
# named volume â managed by Docker, survives 'down'
- db-data:/var/lib/postgresql/data
# bind mount â a host path, read-only
- ./config:/etc/app:ro
# anonymous volume â keeps node_modules out of the bind mount above
- /app/node_modules
# long syntax
- type: bind
source: ./src
target: /app/src
read_only: true
volumes:
db-data:
shared:
external: true # created outside Compose
nfs-data:
driver_opts:
type: nfs
o: addr=10.0.0.10,rw
device: ':/exports/data'
đ Networks
services:
web:
networks:
- frontend
api:
networks:
frontend:
aliases:
- api.internal
backend:
networks:
frontend:
driver: bridge
backend:
internal: true # no outbound access
existing:
external: true
name: some-other-network
đĄ Compose creates a default network per project and every service joins it, so containers already reach each other by service name. Declare networks when you want to separate things.
đ Ports
services:
web:
ports:
- '8080:80' # host:container
- '127.0.0.1:8080:80' # bind to one interface only
- '8080-8090:80-90' # a range
- '80' # random host port
- target: 80 # long syntax
published: '8080'
protocol: tcp
mode: host
đ§Ŧ Environment Variables
services:
app:
environment:
# map form (preferred)
LOG_LEVEL: debug
# take the value from the shell or .env
API_KEY: ${API_KEY}
# with a default, and a hard requirement
PORT: ${PORT:-3000}
DB_URL: ${DB_URL:?DB_URL must be set}
env_file:
- path: .env.production
required: false
# variables come from the shell, .env, and --env-file, in that order of precedence
API_KEY=abc docker compose up -d
docker compose --env-file .env.staging up -d
đŠē Healthchecks & Startup Order
depends_on on its own only waits for the container to start. Wait for it to be healthy instead:
services:
db:
image: postgres:16
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
api:
image: myapi
depends_on:
db:
condition: service_healthy # or service_started, service_completed_successfully
đ Restart Policies
| Policy | Behaviour |
|---|---|
no | Never restart (the default) |
on-failure | Restart only on a non-zero exit â on-failure:5 to cap tries |
always | Always restart, including after a daemon restart |
unless-stopped | Like always, but stays stopped if you stopped it yourself |
đ§Š Profiles
Profiles keep optional services out of the way until you ask for them.
services:
web:
image: nginx # no profile: always started
debug:
image: busybox
command: top
profiles: [debug]
seed:
image: myapp
command: npm run seed
profiles: [tools]
# start the default services plus one profile
docker compose --profile debug up -d
# several at once
docker compose --profile debug --profile tools up -d
# COMPOSE_PROFILES works too
COMPOSE_PROFILES=debug,tools docker compose up -d
đ Watch Mode
docker compose watch syncs or rebuilds automatically as you edit â a dev loop without bind-mount surprises.
services:
web:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
- action: sync+restart
path: ./config
target: /etc/app
docker compose watch
âģī¸ Reuse: include, extends & anchors
# pull in another compose file as if it were written here
include:
- path: ./monitoring/compose.yaml
services:
# inherit another service's definition
worker:
extends:
file: common.yaml
service: base-app
command: ["node", "worker.js"]
# YAML anchors for repeated blocks
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
services:
api:
logging: *default-logging
web:
logging: *default-logging
đ Secrets & Configs
File-based secrets work in plain Compose: each one is mounted read-only at /run/secrets/<name>. External secrets and configs require Swarm.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
configs:
- source: pg_conf
target: /etc/postgresql/postgresql.conf
secrets:
db_password:
file: ./db_password.txt
api_token:
external: true # Swarm only
configs:
pg_conf:
file: ./postgresql.conf
đĨ GPU & Device Access
services:
# NVIDIA â needs the NVIDIA Container Toolkit on the host
cuda-app:
image: nvidia/cuda:12.4.1-base-ubuntu22.04
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all # or: device_ids: ["0"]
capabilities: [gpu]
# Intel iGPU â VAAPI/OpenCL through the render device
igpu-app:
image: intel/openvino
devices:
- /dev/dri:/dev/dri
group_add:
- video
đ Everyday Commands
# start everything in the background
docker compose up -d
# rebuild the images first
docker compose up -d --build
# start one service and what it depends on
docker compose up -d <service>
# recreate containers even if nothing changed
docker compose up -d --force-recreate
# stop and remove containers and networks
docker compose down
# also remove the named volumes and local images
docker compose down -v --rmi local
# what is running, including health
docker compose ps
# follow the logs
docker compose logs -f
docker compose logs -f --tail 100 <service>
# a shell inside a running service
docker compose exec <service> sh
# a one-off container for a task
docker compose run --rm <service> npm test
# rebuild, pull, restart
docker compose build --no-cache
docker compose pull
docker compose restart <service>
# scale a stateless service
docker compose up -d --scale worker=3
# validate and print the effective configuration
docker compose config
docker compose config --services
# processes and resource usage
docker compose top
docker compose stats
âī¸ Compose vs Swarm Stacks
Both read a Compose file, but docker stack deploy honours a different subset of it.
| Key | docker compose | docker stack deploy |
|---|---|---|
build: | â builds locally | â ignored â push the image to a registry first |
restart: | â | â use deploy.restart_policy |
depends_on: | â | â ignored â rely on health checks and retries |
profiles: | â | â |
develop.watch: | â | â |
deploy.replicas/placement | â ignored | â this is how you scale |
configs: / external secrets: | â external ones need Swarm | â |
healthcheck: | â | â also drives rescheduling |
volumes: / networks: | â | â (overlay networks in Swarm) |
đ§ Keep
compose.yamlfor local development and a separatestack.yamlfor Swarm, or put the Swarm-only bits in an override file.