# Docker Compose v2 β annotated reference.
# Docs: https://docs.docker.com/compose/compose-file/
#
# Commands:
# docker compose up -d start in background
# docker compose up --build rebuild images, then start
# docker compose down stop + remove containers, networks
# docker compose down -v also remove named volumes
# docker compose ps list services
# docker compose logs -f web tail logs of `web`
# docker compose exec web sh shell into running container
# docker compose run --rm web python one-off command
# docker compose config validate + print resolved config
#
# File precedence: docker-compose.yml + docker-compose.override.yml are merged
# automatically. Use `-f` to pick explicit files, e.g.
# docker compose -f compose.yml -f compose.prod.yml up
# `version:` is obsolete in Compose v2 β omit it.
# βββ Reusable fragments (YAML anchors) βββββββββββββββββββββββββββββββββββββββ
x-app-defaults: &app-defaults
restart: unless-stopped
env_file: .env
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
services:
# βββ A web app built from a local Dockerfile ββββββββββββββββββββββββββββββ
web:
<<: *app-defaults # merge in shared defaults
build:
context: ./web # path containing the Dockerfile
dockerfile: Dockerfile # default: Dockerfile
target: runtime # multi-stage target
args: # build-time ARGs
NODE_VERSION: "20"
cache_from: ["myorg/web:cache"]
image: myorg/web:latest # tag of the built image (also for pull)
container_name: web # optional, fixed name (loses scaling)
command: ["node", "server.js"] # override CMD (exec form preferred)
entrypoint: ["/usr/local/bin/entry"] # override ENTRYPOINT
working_dir: /app
user: "1000:1000"
ports:
- "8080:80" # host:container
- "127.0.0.1:9229:9229" # bind to loopback only
expose:
- "9000" # expose to linked services only
environment:
NODE_ENV: production
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app # ${} = from .env or shell
env_file:
- .env
- .env.local
volumes:
- ./web:/app # bind mount (source : target)
- node_modules:/app/node_modules # named volume (defined below)
- /var/run/docker.sock:/var/run/docker.sock:ro # read-only bind
- type: tmpfs
target: /tmp
depends_on:
db:
condition: service_healthy # wait until db's healthcheck passes
cache:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/health"]
interval: 10s
timeout: 3s
retries: 5
start_period: 20s # grace period before failures count
deploy: # honored by `docker compose up` for limits
resources:
limits: { cpus: "1.0", memory: 512M }
reservations: { cpus: "0.25", memory: 128M }
networks: [frontnet, backnet]
profiles: ["full"] # only starts when `--profile full` given
# βββ A managed image (Postgres) βββββββββββββββββββββββββββββββββββββββββββ
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password # safer than env var
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
secrets: [db_password]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
retries: 10
networks: [backnet]
cache:
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
networks: [backnet]
# βββ Top-level networks βββββββββββββββββββββββββββββββββββββββββββββββββββββ
networks:
frontnet: # default: bridge driver
backnet:
driver: bridge
internal: true # no external connectivity
ipam:
config:
- subnet: 10.42.0.0/24
# βββ Top-level volumes ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
volumes:
db-data:
node_modules:
# βββ Secrets (read from files; never put plaintext in compose) ββββββββββββββ
secrets:
db_password:
file: ./secrets/db_password.txt