A container being started and the program inside it being ready are different moments. For Postgres on a fresh volume they are seconds apart.
time ─────────────────────────────────────────────────────────►
db │ started │ initdb … restart … │ ready: accepts connections
web │ started │ connect() ──► FATAL: the database system is starting up
└─ exits 1depends_on (short form) orders starts only
services:
web:
build: .
depends_on:
- dbCompose starts db's container first, then web's. It does not wait for Postgres. The app connects at startup, gets an error and exits:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
signup-db-1 postgres:16 "docker-entrypoint.s…" db 2 seconds ago Up 2 seconds 5432/tcp
signup-web-1 signup-web "python app.py" web 2 seconds ago Exited (1) 2 seconds agoweb-1 | Traceback (most recent call last):
web-1 | File "/app/app.py", line 12, in <module>
web-1 | db = connect()
web-1 | psycopg.OperationalError: connection failed: connection to server at "db" (172.18.0.2), port 5432 failed: FATAL: the database system is starting upRunning docker compose up -d again usually works, because Postgres is ready by then. That is a race, not a fix.
Health checks report readiness
A healthcheck runs a command inside the container on a schedule. Exit code 0 means healthy.
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 3s
retries: 10| Field | Meaning |
|---|---|
test | ["CMD", prog, args…] runs directly; ["CMD-SHELL", "…"] runs through sh -c |
interval | time between checks (default 30s) |
timeout | a check that takes longer counts as failed |
retries | consecutive failures before the status becomes unhealthy |
start_period | grace time after start when failures do not count |
docker compose ps shows the status: Up 3 seconds (health: starting), then (healthy).
Wait for healthy
The long form of depends_on takes a condition:
web:
build: .
depends_on:
db:
condition: service_healthy| Condition | web starts when |
|---|---|
service_started | db's container has started (same as the short form) |
service_healthy | db's health check reports healthy |
service_completed_successfully | db exited with code 0 (for one-off jobs such as migrations) |
docker compose up -d now prints Healthy for db before it starts web:
[+] Running 3/3
✔ Network signup_default Created 0.1s
✔ Container signup-db-1 Healthy 2.3s
✔ Container signup-web-1 Started 0.3sRestart policies are a fallback
restart: on-failure makes Docker restart web after it exits non-zero. The app comes up after a few crashes, but every crash adds a traceback to the logs and a restart to the count. Use service_healthy for startup order. Keep a restart policy for crashes later in the container's life.