Make Docker Compose Wait for PostgreSQL Before Django Starts
Stop Django failing with could not connect to server or connection refused when Docker Compose starts it before PostgreSQL is ready: use depends_on with service_healthy, pg_isready health checks, and retry logic in the entrypoint.
Problem statement
On docker compose up, the Django container fails on its first attempt:
web-1 | django.db.utils.OperationalError: connection to server at "db" (172.18.0.2), port 5432 failed: Connection refused
web-1 | Is the server running on that host and accepting TCP/IP connections?
web-1 exited with code 1
A few seconds later PostgreSQL logs database system is ready to accept connections. Plain depends_on: [db] only waits for the database container to start, not for PostgreSQL inside it to accept connections. On first boot, when PostgreSQL initialises the data directory, that gap can be ten seconds or more, and Django's migrate runs straight into it.
- Add a health check to the
dbservice withpg_isready. - Use
depends_on: db: condition: service_healthyin thewebandworkerservices. - Add
restart: unless-stoppedso a transient failure recovers on its own. - For databases outside Compose (managed PostgreSQL), add a short retry loop in the entrypoint.
- Do not use fixed
sleep 10delays; they are too long on fast machines and too short on slow ones.
Step-by-step solution
1. Give the database a health check
services:
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app -h 127.0.0.1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 20s
pg_isready ships with the image and returns 0 only when the server accepts connections. Use -h 127.0.0.1 so it checks TCP, as Django will; during initialisation, the entrypoint runs a temporary server on the socket only, which pg_isready without -h would report as ready too early.
2. Wait for healthy, not started
web:
build: .
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
Compose now starts web only after db reports healthy. Use the same block for Celery workers and Beat.
3. Add retries for databases Compose does not manage
depends_on only knows about services in the same Compose file. With a managed database, or in Kubernetes, wait in the entrypoint:
#!/bin/sh
set -e
python - <<'PY'
import os, sys, time
import psycopg
url = os.environ["DATABASE_URL"]
for attempt in range(30):
try:
psycopg.connect(url, connect_timeout=3).close()
sys.exit(0)
except psycopg.OperationalError as e:
print(f"database not ready ({attempt + 1}/30): {e}", flush=True)
time.sleep(2)
sys.exit("database never became ready")
PY
exec "$@"
The loop gives up after about a minute with a clear message instead of hanging forever, and exec "$@" hands PID 1 to Gunicorn.
4. Keep restart policies as a safety net
Even with health checks, a database restart later in the stack's life drops connections. restart: unless-stopped brings crashed containers back, and Django reconnects on the next request. Combine with CONN_HEALTH_CHECKS = True so reused connections are verified.
5. Move migrations out of the web container
Running migrate in every web container start means several replicas race to migrate at once. Run it once as a separate service that the web service waits for:
migrate:
build: .
command: python manage.py migrate --noinput
depends_on:
db:
condition: service_healthy
restart: "no"
web:
depends_on:
migrate:
condition: service_completed_successfully
Verification check: run docker compose down -v && docker compose up on a development machine (this deletes the volume, so never on production) to force first-boot initialisation; web should start once, without a failed attempt, after db turns healthy.
Explanation
Docker Compose models dependencies as start order. Without a condition, "started" means the container process launched, which for the PostgreSQL image is a shell script that may spend many seconds creating the cluster, running init scripts, and restarting the server before it listens on TCP. The service_healthy condition, added back in Compose v2 and the Compose Specification, ties start order to a real readiness signal. Health checks plus retry logic cover both startup and the case where a dependency disappears later.
Edge cases / notes
version:key. Old Compose file format 3 droppedcondition; the current Compose Specification (noversionkey needed) supports it again.docker compose up webalso starts dependencies and respects conditions.- Swarm ignores
depends_on; rely on retries and restart policies there. - Wrong host. If Django connects to
localhostinstead ofdb, waiting does not help; see the connection guide below.
Next steps
- If Django still cannot connect after the database is ready, see Fix Django Cannot Connect to Postgres in Docker Compose.
- Review a full stack in Deploy Django With Docker Compose in Production.
FAQ
Does depends_on wait for PostgreSQL to be ready?
Not by default. It only waits for the container to start unless you add condition: service_healthy and a health check.
Is a sleep in the entrypoint good enough?
No. A fixed sleep is either wasted time or not enough. Use health checks or a retry loop.
Why use pg_isready with -h 127.0.0.1?
During initialisation PostgreSQL listens only on the socket. Checking TCP matches how Django connects.
What about Redis?
Give it a redis-cli ping health check and the same service_healthy condition.