All posts

A Sane Starting Point for docker-compose.yml

July 5, 2026 · DevTools

docker
docker-compose
devops
containers
yaml

Docker Compose is the fastest way to run a multi-container stack locally, but its YAML is indentation-sensitive and quietly unforgiving. Here's a clean mental model of the file and the mistakes worth avoiding.

The modern shape

You no longer need the version: key — the current Compose specification dropped it. A minimal web + database stack looks like this:

services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
    depends_on:
      - db
    restart: unless-stopped
  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=app
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  pgdata:

Quote your port mappings

This is the classic YAML foot-gun. Written unquoted, a mapping like 22:22 can be parsed as a base-60 number, and 08:80 is an invalid octal. Always quote:

ports:
  - "8080:80"

The left side is the host port, the right is the container port. The Docker Compose Builder quotes these for you automatically.

Bind mounts vs. named volumes

Two things that look similar behave very differently:

volumes:
  - ./html:/usr/share/nginx/html   # bind mount — a host path
  - pgdata:/var/lib/postgresql/data # named volume — managed by Docker

A bind mount maps a directory on your machine into the container — great for live-editing source. A named volume is storage Docker manages for you — the right choice for databases, because it persists across docker compose down and doesn't depend on a host path. Named volumes must also be declared in the top-level volumes: block (the builder collects them for you).

depends_on isn't a health check

depends_on controls start order, not readiness. It ensures db starts before web, but it does not wait for Postgres to accept connections. For that you need a healthcheck or retry logic in your app. Don't assume the database is ready just because the container started.

Restart policies

PolicyBehaviour
noNever restart (default)
alwaysAlways restart, even after a manual stop + daemon restart
on-failureRestart only on a non-zero exit
unless-stoppedRestart unless you explicitly stopped it

unless-stopped is usually what you want for long-running services: it survives crashes and reboots but respects a deliberate docker compose stop.

Build it visually

Getting the indentation and the top-level volumes: block right by hand is exactly the kind of fiddly work that produces silent failures. The Docker Compose Builder turns a form into spec-compliant YAML you can copy or download — and you can paste the result into the YAML Validator if you want a second check. Everything runs in your browser.

Tools mentioned in this post