Deployment
Hooks
Hooks are plain shell scripts in .kamal/hooks/ that wheels deploy runs locally at specific points in a deploy. They’re the escape hatch for “run this thing around every deploy” — Slack notifications, smoke tests, database backups, cache warms. Every hook is optional; if the file doesn’t exist, wheels deploy skips it without a warning.
You’ll learn:
- The three supported hook events
- The
KAMAL_*environment variables every hook receives - How to write and install a hook
- What happens when a hook fails
The three events
Section titled “The three events”| Hook | Fires | Aborts? |
|---|---|---|
pre-deploy | Before any host work starts. | Yes — non-zero exit aborts the deploy before anything on the servers changes. |
post-deploy | After a successful deploy. | Yes — non-zero exit fails the deploy after-the-fact, useful for smoke tests. |
post-deploy-failure | After a deploy that threw an error. | No — it’s best-effort on an already-failed path. A non-zero exit from the hook is logged ([hook:post-deploy-failure] ...) and never replaces the original deploy error; the deploy rethrows the real failure and the overall exit stays non-zero. |
All three live under .kamal/hooks/ and must be executable (chmod +x). They run on the control machine — the same machine running wheels deploy — not on the target hosts.
Environment variables every hook receives
Section titled “Environment variables every hook receives”wheels deploy fires every hook with a KAMAL_* env block that follows Ruby Kamal’s naming. Keeping the prefix KAMAL_ (not WHEELS_) means hooks written for Ruby Kamal that read these variables work unchanged — you can port them between the two tools without editing. The block is a subset of Ruby Kamal’s full contract: variables like KAMAL_ROLE and KAMAL_SERVICE are not set.
| Variable | When available | Value |
|---|---|---|
KAMAL_VERSION | all events | Version being deployed (git short sha by default). |
KAMAL_HOSTS | all events | Comma-separated list of hosts in the deploy. |
KAMAL_PERFORMER | all events | git config user.name, falling back to $USER when unset. |
KAMAL_DESTINATION | all events | Value of --destination, or empty. |
KAMAL_RUNTIME | post-deploy, post-deploy-failure | Seconds elapsed since the deploy started. |
KAMAL_ERROR | post-deploy-failure only | Error message that stopped the deploy. |
Example — Slack notification on success
Section titled “Example — Slack notification on success”#!/usr/bin/env bashset -euo pipefail
curl -sS -X POST "$SLACK_WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d "$(cat <<EOF{ "text": "Deployed \`${KAMAL_VERSION}\` to \`${KAMAL_HOSTS}\` in ${KAMAL_RUNTIME}s by ${KAMAL_PERFORMER}"}EOF)"Remember to chmod +x .kamal/hooks/post-deploy before committing.
Example — Smoke test before deploy
Section titled “Example — Smoke test before deploy”Abort the deploy if staging is broken:
#!/usr/bin/env bashset -euo pipefail
if ! curl -sf --max-time 5 https://staging.example.com/up > /dev/null; then echo "Staging is down; refusing to deploy" >&2 exit 1fiThe non-zero exit stops wheels deploy before it touches production.
Example — Page on failure
Section titled “Example — Page on failure”#!/usr/bin/env bash# No `set -e`: a paging failure isn't worth extra noise in the deploy# log. (Even if this hook exits non-zero, wheels deploy only logs it —# the original deploy error always survives.)
curl -sS -X POST https://events.pagerduty.com/v2/enqueue \ -H 'Content-Type: application/json' \ -d "$(cat <<EOF{ "routing_key": "${PAGERDUTY_KEY}", "event_action": "trigger", "payload": { "summary": "Deploy of ${KAMAL_VERSION} failed: ${KAMAL_ERROR}", "severity": "error", "source": "wheels deploy" }}EOF)" || trueOutput
Section titled “Output”Hook stdout and stderr are merged and prefixed with [hook:<name>] in the deploy log:
[hook:pre-deploy] staging check passed[hook:post-deploy] slack notification sentA non-zero exit from pre-deploy aborts the deploy before any server work. A non-zero exit from post-deploy marks the deploy as failed after-the-fact — the containers are already rolled over, so this is useful for integration smoke tests that gate “did the deploy actually work” rather than “can I start the new container.”
post-deploy-failure runs on an already-failed path, so the overall exit stays non-zero either way. The hook is best-effort: a non-zero exit from the hook itself is logged as [hook:post-deploy-failure] Hook post-deploy-failure exited with code N (ignored — surfacing the original deploy error) and the original deploy error rethrows untouched.
Debugging hooks
Section titled “Debugging hooks”Hooks run in the project root directory (where deploy.yml lives). $USER is the user who invoked wheels deploy. Everything else is your environment.
To see exactly what env vars your hook receives, drop this in during development:
#!/usr/bin/env bashenv | grep ^KAMAL_To run a hook manually with the expected env:
KAMAL_VERSION=abc1234 \KAMAL_HOSTS=192.0.2.10 \KAMAL_RUNTIME=42 \KAMAL_PERFORMER=$USER \./.kamal/hooks/post-deploy