Skip to content

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
HookFiresAborts?
pre-deployBefore any host work starts.Yes — non-zero exit aborts the deploy before anything on the servers changes.
post-deployAfter a successful deploy.Yes — non-zero exit fails the deploy after-the-fact, useful for smoke tests.
post-deploy-failureAfter 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.

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.

VariableWhen availableValue
KAMAL_VERSIONall eventsVersion being deployed (git short sha by default).
KAMAL_HOSTSall eventsComma-separated list of hosts in the deploy.
KAMAL_PERFORMERall eventsgit config user.name, falling back to $USER when unset.
KAMAL_DESTINATIONall eventsValue of --destination, or empty.
KAMAL_RUNTIMEpost-deploy, post-deploy-failureSeconds elapsed since the deploy started.
KAMAL_ERRORpost-deploy-failure onlyError message that stopped the deploy.
.kamal/hooks/post-deploy (illustrative — do not type)
#!/usr/bin/env bash
set -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.

Abort the deploy if staging is broken:

.kamal/hooks/pre-deploy (illustrative — do not type)
#!/usr/bin/env bash
set -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 1
fi

The non-zero exit stops wheels deploy before it touches production.

.kamal/hooks/post-deploy-failure (illustrative — do not type)
#!/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
)" || true

Hook stdout and stderr are merged and prefixed with [hook:<name>] in the deploy log:

deploy log output (illustrative)
[hook:pre-deploy] staging check passed
[hook:post-deploy] slack notification sent

A 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.

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:

.kamal/hooks/post-deploy (illustrative — do not type)
#!/usr/bin/env bash
env | grep ^KAMAL_

To run a hook manually with the expected env:

manual hook invocation (illustrative — do not type)
KAMAL_VERSION=abc1234 \
KAMAL_HOSTS=192.0.2.10 \
KAMAL_RUNTIME=42 \
KAMAL_PERFORMER=$USER \
./.kamal/hooks/post-deploy