Use service_healthy gating in Compose — prerequisites, decision, and a limitation

Use service_healthy gating in Compose — prerequisites, decision, and a limitation

calendar_today ago • schedule2 min read
— Originally published at chriseugenerodriguez.com

Prerequisites

  • You control the service image (or can add a lightweight wrapper) and can add a healthcheck that exercises the real readiness condition.
  • You use Docker Compose v2-style depends_on conditions (support varies by Compose implementation/version).

Implementation decision

Put the readiness check on the dependency, not on the dependent. Define a healthcheck that verifies the real work boundary (for example, a small query or a completed bootstrap signal). Then set the dependent’s depends_on to use condition: service_healthy. This makes Compose wait for the backend’s own claim of readiness instead of relying on timed waits.

Expected result

Compose starts services in right order and then blocks the dependent until the backend reports healthy. This eliminates the startup race where the dependent is running and fails because its backend is still booting. If the backend lacks a healthcheck, Compose can’t honor service_healthy — which surfaces the missing readiness contract rather than silently permitting a race.

One limitation

A healthcheck must be honest and relevant. If it only checks that a TCP port is open, it may still return healthy before the backend can serve the app’s real needs. When the healthcheck is superficial, the gating gives you a false sense of correctness. Keep the check aligned to the actual dependency boundary.

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

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Free SEO Tools Every Small Business Should Use (2026) | FloxoLab

floxolab - Oct 20

My 30-day log cleanup kept a file that was 30 and a half days old

BashSnippets - Oct 8

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

A Textile Mill's Dyehouse Ramp Curve: Get One Step Wrong and the Sheet Belongs to the Master

aiomniu - Oct 18

I escaped the dot correctly and sed still rewrote a file marked DO NOT CHANGE

BashSnippets - Oct 13
chevron_left
342 Points • 15 Badges
Santa Monica, CA • chriseugenerodriguez.com
10Posts
0Comments
2Connections
10 years fe

Related Jobs

Commenters (This Week)

1 comment
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!