Problem summary

Docker Compose guarantees startup order but not readiness. A dependency can be “running” long before it’s able to answer the queries or complete the bootstrap your app needs. People often patch this with sleeps or shallow probes — fragile approaches that pass locally and fail in the field.

Implementation approach (high level)

1) Define a healthcheck on the service that knows how to prove it’s truly ready. The check must exercise the actual boundary your dependent relies on (example: a simple query, a bootstrap-complete flag, or an authentication handshake), not merely an open port.

2) Make the dependent wait using depends_on with condition: service_healthy. That moves the readiness contract into the dependency and asks Compose to gate the dependent start on the dependency’s own proof.

Expected behavior

  • If the backend is running and its healthcheck reports healthy, the dependent is allowed to start.
  • If the backend is running but has no healthcheck (or the check fails), Compose cannot honor service_healthy and will block the dependent. This makes the missing signal visible instead of hiding it behind timing guesses.
  • If the backend is still in a starting state, the dependent remains blocked until the backend transitions to healthy.

Limitations and caveats

  • A healthcheck is only effective when it actually verifies the work the dependent needs. Superficial probes (e.g., “is port open?”) can still return success too early and recreate the race.
  • service_healthy is a startup gating mechanism. It reduces early-launch races but does not replace application-level error handling and retry logic for transient failures after startup.
  • This pattern depends on honest healthcheck design. If you’re unable to add meaningful checks to a third-party image, consider a lightweight init wrapper or a sidecar that verifies readiness on that service’s behalf.

Why this is better than sleep loops

Sleep loops are guesses about time and hide the contract. Health-based gating expresses the contract explicitly: the backend must prove readiness. That makes startup behavior observable, debuggable, and consistent across environments.

The article includes a tiny deterministic fixture (no Docker required) that proves the decision boundary between “running” and “healthy” and a checklist for what to put in a real Compose file. The server will append the verified Compose example immediately after this content.

Working example

export function evaluateStartability(services, targetServiceName) { if (!services || typeof services !== 'object' || Array.isArray(services)) { throw new TypeError('services must be an object') } const target = services[targetServiceName] if (!target) throw new Error(`service not found: ${targetServiceName}`) const deps = target.depends_on ?? {} const normalizedDeps = {} if (Array.isArray(deps)) { for (const name of deps) normalizedDeps[name] = { condition: 'service_started' } } else { for (const [name, val] of Object.entries(deps)) { if (val == null || val === 'short') { normalizedDeps[name] = { condition: 'service_started' } } else if (typeof val === 'string') { normalizedDeps[name] = { condition: val } } else { normalizedDeps[name] = { condition: val.condition ?? 'service_started' } } } } const reasons = [] for (const [depName, opts] of Object.entries(normalizedDeps)) { const dep = services[depName] if (!dep) { reasons.push({ dep: depName, ok: false, why: 'missing' }) continue } if (dep.state !== 'running') { reasons.push({ dep: depName, ok: false, why: `not running (${dep.state})` }) continue } if (opts.condition === 'service_healthy') { if (!Object.prototype.hasOwnProperty.call(dep, 'health') || dep.health == null) { reasons.push({ dep: depName, ok: false, why: 'no healthcheck defined' }) continue } if (dep.health !== 'healthy') { reasons.push({ dep: depName, ok: false, why: `health=${String(dep.health)}` }) continue } } reasons.push({ dep: depName, ok: true, why: 'satisfied' }) } return { ok: reasons.every((r) => r.ok), reasons } } export function makeService(state = 'running', health = null, depends_on = undefined) { return { state, health, depends_on } } 

Read the full article: https://chriseugenerodriguez.com/blog/compose-wait-for-ready-service