AI

Docker Compose depends_on Not Working: Postgres Startup Fix

Docker Compose depends_on Not Working: Postgres Startup Fix

Your app crashes on startup. Postgres logs show it’s running. The connection still fails. Every time.

This is the depends_on race condition โ€” one of the most deceptively frustrating problems in containerized development. The fix exists. It’s not obvious. And most tutorials get it halfway right.

Key Takeaways

  • Docker Compose’s depends_on with condition: service_started only waits for the container process to launch, not for Postgres to actually accept connections โ€” making a healthcheck the only reliable fix.
  • Solving the startup race condition requires combining condition: service_healthy with a properly configured pg_isready healthcheck, not just container ordering.
  • According to Docker’s official documentation, service_healthy has been available since Compose spec v3.4 (2018), yet misconfiguration remains one of the top reported issues in Docker community forums as of Q1 2026.
  • Teams using Kubernetes encounter the same race condition through readiness probe misconfiguration โ€” the underlying problem is identical.
  • A correct healthcheck config reduces failed startup attempts from potentially dozens of retries to near-zero in CI/CD pipelines.

What’s Actually Happening at Startup

Postgres doesn’t go from “container started” to “accepting connections” instantly. There’s a real sequence: the container spawns, the init system runs, Postgres loads shared memory, replays WAL if needed, and then opens the socket for TCP connections. That entire process can take anywhere from 1 to 15 seconds depending on hardware and database size.

Docker Compose’s default depends_on behavior checks exactly one thing: whether the container process started. Not whether it’s ready. Not whether port 5432 is listening. Just that the PID exists.

So your app container starts, tries to connect immediately, and hits a socket that isn’t open yet. Connection refused. Crash. The container restarts. Maybe it works on retry. Maybe it doesn’t. This is non-deterministic โ€” which makes it especially painful to debug.

The good news: Docker Compose has had a proper fix since the Compose spec added condition: service_healthy support. The bad news: it requires explicit healthcheck configuration that most tutorials skip entirely.


Why depends_on Alone Doesn’t Cut It

The root cause is a semantic gap between “container running” and “service ready.” Docker’s architecture deliberately separates these two concepts.

When you write:

depends_on:
  - postgres

Compose translates that to condition: service_started internally. The app container gets a green light the moment the Postgres container’s entrypoint process launches. Postgres itself might still be mid-initialization.

Docker’s official Compose specification is explicit about this. service_started means: “Wait until a container with a matching name or label is running.” Running. Not healthy. Not ready.

service_healthy is different. It holds the dependent container in a waiting state until the target container’s healthcheck returns exit code 0. No healthcheck defined? The condition can never be satisfied, and Compose will either throw an error or fall back to service_started depending on version.

This is where most engineers hit the wall. They add condition: service_healthy without defining a healthcheck on the Postgres service. Compose either errors out or the condition is never met โ€” so the app still starts prematurely.


The Complete Fix: Healthcheck + Condition Together

Getting this right requires two changes working in tandem.

The Postgres Service Healthcheck

Add this to your Postgres service definition:

postgres:
  image: postgres:16
  environment:
    POSTGRES_USER: myuser
    POSTGRES_PASSWORD: mypassword
    POSTGRES_DB: mydb
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
    interval: 5s
    timeout: 5s
    retries: 5
    start_period: 10s

pg_isready is the right tool here. It’s a Postgres-native utility that checks whether the server is ready to accept connections on the specified user and database โ€” not just whether the port is open. According to Last9’s Docker health monitoring analysis, using pg_isready instead of a raw TCP check catches cases where Postgres is listening but hasn’t finished setting up the target database yet.

The start_period: 10s parameter matters more than people realize. It tells Docker not to count failed healthchecks during the first 10 seconds of container life. Without it, Compose may mark the container as unhealthy before Postgres even finishes initializing โ€” triggering unnecessary restarts before the process has a fair shot.

The App Service Dependency

app:
  build: .
  depends_on:
    postgres:
      condition: service_healthy

That’s it. Compose will now poll the Postgres healthcheck every 5 seconds, wait for a clean success, and only then start the app container.

Dependency Strategies Compared

StrategyWhat It Waits ForReliable?Config Complexity
depends_on: [postgres]Container process startโŒ NoMinimal
depends_on + service_startedContainer process startโŒ NoMinimal
depends_on + service_healthy (no healthcheck)Undefined / errorโŒ NoLow
depends_on + service_healthy + pg_isreadyPostgres accepting connectionsโœ… YesModerate
External wait scripts (wait-for-it.sh)TCP port openโš ๏ธ PartialHigh

External wait scripts like wait-for-it.sh are a common workaround, but they check TCP connectivity โ€” not application readiness. Postgres can accept a TCP handshake while still refusing database connections. The native healthcheck approach is cleaner and more accurate.


Real-World Scenarios and What to Do

CI/CD pipelines failing intermittently

This is the most common production symptom. A GitHub Actions or GitLab CI pipeline runs integration tests. 70% of the time it works. 30% of the time the app can’t connect to Postgres on startup and the tests fail. The fix is the healthcheck config above, applied consistently across all Compose files used in CI. According to OneUptime’s January 2026 Docker Compose health check guide, adding start_period to account for slower CI runner hardware is especially important โ€” Postgres startup in shared CI environments can run 2โ€“3x slower than local dev.

Local development restarts causing connection errors

When you run docker compose restart app, the app might come back up before Postgres finishes its own restart cycle. The healthcheck fixes this too. The condition: service_healthy dependency applies on restarts, not just initial startup.

Multi-service apps with migrations

If your app runs database migrations on startup โ€” common with tools like Flyway or Alembic โ€” a race condition here is catastrophic. Migrations fail, the schema is left in a partial state, and subsequent restarts compound the damage. The healthcheck approach gives migrations a clean, ready database every time. This isn’t always the answer if your migration tool has its own retry logic, but for most setups it removes an entire class of failure.


What Comes Next

Docker Desktop 4.x, the dominant dev environment as of April 2026, supports the full healthcheck spec with no additional configuration. The tooling is there. Adoption is still inconsistent.

Over the next 6โ€“12 months, expect Docker Compose to add better warnings when condition: service_healthy is declared without a corresponding healthcheck. There’s an open feature request in the Docker Compose GitHub repository (as of early 2026) to surface this as a validation error rather than silent misbehavior.

The race condition isn’t going away. But the fix is now yours.

Check your existing Compose files. If depends_on doesn’t have condition: service_healthy paired with a working pg_isready healthcheck, it’s probably just lucky timing holding things together โ€” and that luck will eventually run out in CI.

References

  1. Docker Status Unhealthy: What It Means and How to Fix It | Last9
  2. How to Create Docker Compose Health Checks

Photo by Rubaitul Azad on Unsplash