Why does a Kamal deploy hang on the health check?
The new container started and exited before Rails could answer /up. Kamal waits, then rolls back and leaves the old container serving. Run kamal app logs to see the real error. On a first deploy it is almost always a missing or wrong RAILS_MASTER_KEY, which raises before any route exists to answer the check.
Read the container's own logs first
Kamal's output tells you the health check failed. It does not tell you why, because the failure happened inside the container. That is a separate log:
bin/kamal app logs --lines 100
bin/kamal app logs --lines 100 --grep Error
Nine times out of ten the last lines are a Ruby backtrace from boot, and the error is legible.
The three that account for most of them
A missing or wrong RAILS_MASTER_KEY. Rails cannot decrypt config/credentials.yml.enc, so it
raises during initialization. Nothing is listening on the port, so the health check gets connection
refused rather than a 500. Confirm the key is actually reaching the server:
bin/kamal secrets print
If the value is blank, .kamal/secrets is not exporting it. That file is not committed, so a fresh
clone or a new CI runner will not have it.
A database the container cannot write. One Shot runs on SQLite, and the database file lives on a
mounted volume. If the volume is missing or owned by the wrong user, the first write raises and the
boot aborts. bin/kamal app exec 'ls -la db' shows what the container actually sees.
A port mismatch. The health check asks the container for /up on the port Kamal thinks it
exposes. If you changed the Puma port without changing config/deploy.yml, the check talks to
nothing.
What Kamal does while you debug
Nothing bad. The old container keeps serving traffic for the whole wait, and a failed deploy leaves it in place. That is the behavior you want: a deploy that cannot boot does not take the site down, it just fails.
The check that catches it before the deploy
The same boot happens locally with the production environment. This is faster than a round trip to the server:
RAILS_ENV=production bin/rails runner 'puts "boots"'
If that raises, the deploy will too, for the same reason.