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
Top comments (0)