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.

The startup racedb container updepends_on satisfiedweb startsruns migratePostgres bootingnot accepting yetConnection refusedweb exitsPlain depends_on waits for the container, not for PostgreSQL to accept connections.
  1. Add a health check to the db service with pg_isready.
  2. Use depends_on: db: condition: service_healthy in the web and worker services.
  3. Add restart: unless-stopped so a transient failure recovers on its own.
  4. For databases outside Compose (managed PostgreSQL), add a short retry loop in the entrypoint.
  5. Do not use fixed sleep 10 delays; 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.

what Compose waits fordepends_on: [db]✕waits for the container to start✕Postgres may still be initialising✕migrate hits connection refusedcondition: service_healthy✓waits for pg_isready to pass✓Postgres accepts TCP connections✓web starts only when it can connectHealthy means accepting connections, which is what Django needsdjango-deployment.com
The condition form needs a health check on the db service to work.

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
start order that never races1db and redis starthealth checks begin2pg_isready passesservice_healthy3migrate runs onceservice_completed_successfully4web, worker, beat startdatabase ready and migrated5proxy routes trafficweb healthyEach service starts only when what it needs is readydjango-deployment.com
A separate migrate service also stops replicas racing to apply migrations.

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 dropped condition; the current Compose Specification (no version key needed) supports it again.
  • docker compose up web also starts dependencies and respects conditions.
  • Swarm ignores depends_on; rely on retries and restart policies there.
  • Wrong host. If Django connects to localhost instead of db, waiting does not help; see the connection guide below.

Next steps

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.

Keep going