✦ For everyone, free.

Practical knowledge for real and everyday life

Home

16.4.1.5 Compose Dependency Not Ready

A focused guide to Compose Dependency Not Ready, connecting core concepts with practical Docker and container operations.

Compose dependency not ready describes the specific failure where a service starts and attempts to use a dependency that, despite depends_on being configured, is still not actually capable of handling requests at the moment the dependent service makes its first attempt, a gap that persists even with a health condition configured, because health check timing and a dependency's true readiness for the very first real interaction do not always align perfectly.

Why a passing health check does not guarantee the very next moment is safe

A health check reporting healthy confirms the dependency passed its check at that specific sampling instant, but there is necessarily some delay between that check succeeding and the dependent service's own first connection attempt, and in rare cases a dependency can experience a brief, transient hiccup in exactly that narrow window:

services:
  api:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s

This configuration is correct and necessary, but it reduces the probability of a dependency-not-ready race condition considerably rather than eliminating it with absolute certainty, which is why application-level retry logic remains a valuable complement even when depends_on health conditions are already correctly configured.

Retry logic as the necessary complement

Because no startup ordering mechanism can provide an absolute guarantee against every possible transient timing issue, the application itself retrying a failed initial connection attempt, rather than crashing immediately on the first failure, closes this remaining gap directly:

async function connectWithRetry(retries = 5, delayMs = 2000) {
  for (let i = 0; i < retries; i++) {
    try {
      return await db.connect();
    } catch (err) {
      if (i === retries - 1) throw err;
      console.warn(`Connection attempt ${i + 1} failed, retrying...`);
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
}

This is not a substitute for correct depends_on configuration, since starting before a dependency has even begun initializing would require many more retries and a much longer delay than a brief, narrow timing race would; the two mechanisms address different parts of the same overall problem, coarse-grained ordering and fine-grained transient resilience.

Older Compose versions and depends_on condition support

Health-aware depends_on conditions require a sufficiently current version of the Compose specification and tooling; an older Compose file format, or certain older tooling versions, support only the basic, weaker form of depends_on that guarantees container start order but says nothing about actual readiness:

version: '2'
services:
  api:
    depends_on:
      - db
services:
  api:
    depends_on:
      db:
        condition: service_healthy

Confirming the Compose file format version, or migrating away from an older version field entirely toward the current specification, is worth checking directly if health-aware conditions appear to have no effect at all, since this can indicate the configuration is being silently ignored by tooling that does not support the specific syntax being used.

Using a dedicated wait script as a complement

For dependencies without a convenient or reliable built-in health check mechanism, or for additional certainty beyond what depends_on alone provides, a dedicated wait script run as part of the dependent service's own entrypoint can perform an explicit, blocking readiness check before the actual application starts:

#!/bin/sh
until nc -z db 5432; do
  echo "Waiting for database..."
  sleep 1
done
exec "$@"
ENTRYPOINT ["/wait-for-db.sh"]
CMD ["node", "server.js"]

This pattern, sometimes implemented using a dedicated tool rather than a custom script, provides a final, explicit gate immediately before the application itself starts, which is a useful additional layer particularly for dependencies that take a genuinely variable, hard-to-predict amount of time to become ready.

Diagnosing whether this is actually the cause of an observed failure

When a service fails intermittently, sometimes succeeding and sometimes failing on startup, with the failure correlating with how quickly the dependency became ready relative to the dependent service's own start, this specific timing-related cause is a strong candidate, distinct from a deterministic, every-time configuration error:

docker compose logs db api | grep -E "ready|started|connect"

Reviewing the interleaved timestamps of the dependency reporting itself ready versus the dependent service's first connection attempt, across several different stack startup runs, confirms whether the timing margin is narrow enough that a brief, occasional race is plausible, as opposed to a consistent, deterministic ordering problem that would point toward a missing or misconfigured depends_on condition entirely.

Increasing the margin through health check tuning

Where the readiness race is genuinely narrow and observed to actually cause intermittent failures, tuning the dependency's own health check to be slightly more conservative, requiring an additional successful check or two before reporting healthy, widens the margin between "passed its health check" and "genuinely, robustly ready":

services:
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s
      retries: 1
      start_period: 10s

Adjusting these specific parameters to better match the dependency's actual, observed behavior under realistic startup conditions reduces, though does not entirely eliminate, the residual timing gap that retry logic ultimately needs to be relied upon to close completely.

Common mistakes

  • Assuming a health-aware depends_on condition provides an absolute guarantee against any possible timing issue, rather than a significant, but not perfect, reduction in the likelihood of one.
  • Not implementing retry logic in the dependent application itself, relying entirely on startup ordering configuration to handle every possible timing scenario.
  • Using an outdated Compose file format or tooling version that does not actually support health-aware depends_on conditions, silently falling back to the weaker, start-order-only guarantee.
  • Treating an intermittent, timing-correlated startup failure as a deterministic configuration bug, rather than recognizing the specific signature of a narrow, occasional readiness race.
  • Not reviewing interleaved logs across both the dependency and the dependent service to confirm whether observed failures genuinely correlate with timing margins.

Compose dependency not ready issues are best addressed with a layered approach, health-aware depends_on conditions to handle the coarse-grained ordering, combined with application-level retry logic to absorb whatever narrow, occasional timing gap remains even with correct configuration, since no single mechanism on its own provides an absolute guarantee against every possible startup timing scenario.