## 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:

```bash
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:

```bash
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:

```bash
RAILS_ENV=production bin/rails runner 'puts "boots"'
```

If that raises, the deploy will too, for the same reason.
