Skip to content
Blog

Anatomy of a docker-compose.yml File

Walk through every section of a Compose file — services, build, depends_on, ports, networks, volumes, secrets — with a fully annotated example.

Published on • October 18, 2026

AI Assistant

A Compose file is one declarative YAML document that describes your whole application: services, networks, volumes, configs, and secrets. docker compose up reads it, creates everything in dependency order, and gives you the stack with one command. This walks the Compose file reference section by section, using the Compose Specification format — the merged, current version implemented by Compose v2 (CLI 1.27.0 and later).

Top-level structure

A Compose file has six possible top-level keys, and only one is mandatory:

  • services — required. A map of service names to container definitions. Everything else is optional.
  • networks — bridge/overlay networks your services join.
  • volumes — named volumes for persistent data.
  • configs and secrets — files and credentials injected at runtime without rebuilding images.
  • name — pins the project name (otherwise it’s derived from the directory).

Here is the whole file with all of it in place:

name: shop

services:
  proxy:
    image: nginx:1.27
    ports:
      - "80:80"
    volumes:
      - type: bind
        source: ./proxy/nginx.conf
        target: /etc/nginx/conf.d/default.conf
        read_only: true
    depends_on:
      backend:
        condition: service_started
    networks:
      - front

  backend:
    build:
      context: ./backend
      target: builder
    command: bundle exec thin -p 3000
    environment:
      RACK_ENV: production
    env_file:
      - ./backend/.env
    depends_on:
      db:
        condition: service_healthy
        restart: true
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    networks:
      - back
      - front
    secrets:
      - source: api_token
        target: /run/secrets/api_token
        mode: 0400

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: shop
      POSTGRES_DB: shopdb
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - back
    restart: unless-stopped

networks:
  front: {}
  back: {}

volumes:
  db-data:

secrets:
  api_token:
    file: ./secrets/api_token.txt

The services section

A service definition is the configuration applied to every container of that service — images and runtime arguments, so all replicas are identical.

image vs build. A service needs one or the other. image: postgres:18 pulls; build: compiles from a Dockerfile. The build block takes a context and can select a multi-stage target (builder above), plus dockerfile, args, and cache_from. If both are present, Compose builds and tags the result as image. The image value follows the OCI addressable format: [<registry>/][<project>/]<image>[:<tag>|@<digest>] — pin digests (image: redis@sha256:...) when reproducibility matters.

command and entrypoint. command overrides the image’s CMD; entrypoint overrides its ENTRYPOINT, and a non-null entrypoint discards the image’s default command entirely. Unlike Dockerfile CMD, command doesn’t automatically run through the image’s SHELL — shell features like variable expansion need an explicit command: /bin/sh -c '...'.

environment vs env_file. environment is a map (or KEY=value list); env_file loads whole .env files, processed top-down with later files winning. environment overrides env_file even for empty values. Quote booleans (SHOW: "true") or YAML will turn them into True.

ports vs expose. ports publishes to the host ("80:80"). expose only documents internal container ports for linked services — and the container’s own listening ports are already visible network-wide regardless.

depends_on — short and long form. The short form (- db) controls startup and shutdown ordering only: db starts before web, web is removed before db, but nothing waits for readiness. The long form closes that gap:

depends_on:
  db:
    condition: service_healthy   # also: service_started, service_completed_successfully
    restart: true

service_healthy waits for the dependency’s healthcheck to pass — which is why the db service above defines one. required: false downgrades a missing dependency to a warning.

healthcheck. Mirrors the Dockerfile HEALTHCHECK but lives in YAML: test is ["CMD", ...], ["CMD-SHELL", ...], or NONE (a string is shorthand for CMD-SHELL), with interval, timeout, start_period, and retries.

Also worth knowing: restart (no, always, on-failure, unless-stopped), init: true to inject a PID 1 that reaps zombies and forwards signals, logging:, deploy: resource limits, cap_add/cap_drop, devices:, extra_hosts:, and container_name — but note that a fixed container_name forbids scaling the service beyond one replica.

The networks section

Without any networks: key, every service joins an implicit default network — which the reference shows is literally equivalent to declaring networks: {default: {}}. Declaring named networks is how you segment tiers:

services:
  backend:
    networks:
      back-tier:
        aliases:
          - database
      admin:
        aliases:
          - mysql

Service names are hostnames on shared networks, aliases add alternatives, and ipv4_address pins static IPs (the network then needs matching ipam subnet config). network_mode: host or none bypasses networks entirely — the two attributes are mutually exclusive and Compose rejects files containing both.

The volumes, configs, and secrets sections

Named volumes outlive docker compose down and are declared under top-level volumes: — db-data: with an empty value gets driver defaults; external: true adopts a volume that already exists.

configs and secrets ship files into containers without baking them into images. Short syntax mounts at /<name>; long syntax gives you control:

secrets:
  api_token:
    source: api_token
    target: /run/secrets/api_token
    uid: "103"
    gid: "103"
    mode: 0440

A Compose file errors if a referenced secret doesn’t exist on the platform, so external: true resources must be provisioned beforehand.

Interpolation and .env files

Anywhere in the file you can write ${VAR} or ${VAR:-default} — Compose interpolates from the shell environment and a .env file next to the Compose file. In .env itself: # comments, = or : as delimiter, single quotes are literal (no interpolation), double quotes interpolate and support \n/\t escapes.

Running it

docker compose up -d          # create and start in dependency order
docker compose ps             # status
docker compose logs -f api    # stream one service
docker compose build --no-cache api
docker compose run --rm api rake db:migrate   # one-off command
docker compose down            # stop and remove containers + default network

The same file runs unchanged in development, CI, and production — that portability is the whole point of the Compose application model.

Sources