basecamp/kamal · critical · Kamal::Cli::Healthcheck::Error
container not ready after #{KAMAL.config.deploy_timeout} sec
Error message
container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status}) What it means
During `kamal deploy` (and healthcheck-driven operations), Kamal::Cli::Healthcheck::Poller polls the app container's status on each host via the provided block (e.g. `docker inspect` health/running state). If the status is neither 'running' nor 'healthy' (e.g. 'unhealthy', 'exited', 'restarting') when the overall deadline (KAMAL.config.deploy_timeout seconds) expires, it raises Kamal::Cli::Healthcheck::Error with the last observed status. The poller retries with a growing sleep until time runs out, then surfaces this error, failing the deploy before traffic is switched.
Source
Thrown at lib/kamal/cli/healthcheck/poller.rb:22
def wait_for_healthy(&block)
attempt = 1
timeout_at = Time.now + KAMAL.config.deploy_timeout
readiness_delay = KAMAL.config.readiness_delay
begin
status = block.call
if status == "running"
# Wait for the readiness delay and confirm it is still running
if readiness_delay > 0
info "Container is running, waiting for readiness delay of #{readiness_delay} seconds"
sleep readiness_delay
status = block.call
end
end
unless %w[ running healthy ].include?(status)
raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status})"
end
rescue Kamal::Cli::Healthcheck::Error => e
time_left = timeout_at - Time.now
if time_left > 0
sleep [ attempt, time_left ].min
attempt += 1
retry
else
raise
end
end
info "Container is healthy!"
end
private
def info(message)
SSHKit.config.output.info(message)View on GitHub (pinned to eee0083b38)
Solutions
- Read the status in the message: 'unhealthy' → fix the Docker HEALTHCHECK (path, port, curl/wget present in image); 'exited'/'restarting' → check `kamal app logs -h <host>` for the crash.
- Test the healthcheck manually on the host: `docker exec <container> curl -fsS localhost:3000/up` (or your configured path) to see the real response.
- Raise the deadline in config/deploy.yml: `deploy_timeout: 300` for slow-booting apps.
- If the healthcheck is wrong (route moved, port changed), fix the Dockerfile/composer healthcheck and rebuild, then redeploy.
Example fix
# before (Dockerfile) HEALTHCHECK --interval=1s --timeout=1s CMD curl -fsS http://localhost:3000/up || exit 1 # image has no curl -> always 'unhealthy' # after HEALTHCHECK --interval=1s --timeout=1s CMD wget -qO- http://localhost:3000/up || exit 1 # or, if the app is just slow to boot (config/deploy.yml): # deploy_timeout: 300
Defensive patterns
Strategy: retry
Validate before calling
# Before deploying, verify the healthcheck passes inside the image:
# docker build -t app:test . && docker run -d --name t app:test && sleep 5
# docker inspect --format '{{.State.Health.Status}}' t # expect healthy
# docker rm -f t Type guard
def container_healthy?(image, wait: 10)
`docker run -d --name hc-check #{image} >/dev/null` or return false
sleep wait
status = `docker inspect --format '{{.State.Health.Status}}' hc-check`.strip
`docker rm -f hc-check >/dev/null`
status == "healthy"
end Try / catch
begin
Kamal::CLI::Deploy.new.perform
rescue Kamal::Cli::Healthcheck::Error => e
status = e.message[/\((.*)\)\z/, 1]
warn "container status at timeout: #{status}"
`kamal app logs` if %w[exited restarting].include?(status) # crash vs bad healthcheck
raise
end Prevention
- Add a Docker HEALTHCHECK with tools guaranteed present in the image (wget/curl) and test it in CI.
- Set deploy_timeout above your app's worst-case boot (migrations, cache warmup).
- Verify the health endpoint path/port from inside the container before rolling deploys.
When it happens
Trigger: `kamal deploy` where the new container starts but its Docker HEALTHCHECK never reports healthy within deploy_timeout (missing curl in image, wrong healthcheck path/port), or the container exits/restarts (crash on boot). Also triggered with an unrealistic deploy_timeout for slow-booting apps; status is interpolated into the message so 'unhealthy' vs 'exited' tells you which case it is.
Common situations: Dockerfile healthcheck curling /up (Rails 7.1) while the image lacks curl or the app binds another port; app boots slowly (migrations, warmup) exceeding the default timeout; DB unreachable from the host so the readiness endpoint returns 500; healthcheck path returns 404 after a route change.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Failed to get endpoint for #{role} on #{host}, did the conta
- Failed to get endpoint for #{role} on #{host}, did the conta
- Hook `#{hook}` failed:\n#{e.message}
- Timed out waiting for deploy lock
- Docker is not installed locally
AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21).
Data as JSON: /api/errors/b4fccbd6f18b6342.
Report an issue: GitHub.