A Sane Starting Point for docker-compose.yml
July 5, 2026 · DevTools
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
| Policy | Behaviour |
|---|---|
no | Never restart (default) |
always | Always restart, even after a manual stop + daemon restart |
on-failure | Restart only on a non-zero exit |
unless-stopped | Restart 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.