Most Helm tutorials stop at templating manifests and calling it a day. But production deployments rarely fit neatly into “render, apply, done.” You need database migrations to run before the new version spins up. You need a smoke test after install. You need to back up data before a risky upgrade touches anything. Helm hooks are the mechanism that makes all of this possible — and they’re surprisingly underused.
With Helm hooks, you can inject Kubernetes Jobs, ConfigMaps, or any other resource at specific points in a release’s lifecycle. They run before or after install, upgrade, rollback, and delete operations. With Helm 4 now the current line (v4.2.3 at time of writing), the hooks system has matured alongside improved release tracking and kstatus-based readiness checks.
This walkthrough covers the hook types available, how to wire them up with practical YAML, ordering with weights, cleanup with deletion policies, and the patterns that work in production.
The Nine Hook Points
Helm defines hooks at strategic points in a release lifecycle. Each hook is just a standard Kubernetes manifest with annotations that tell Helm when to execute it:
- pre-install / post-install — runs after templates render but before resources are created, and after all resources are loaded respectively
- pre-upgrade / post-upgrade — same pattern but during
helm upgrade - pre-rollback / post-rollback — triggered during
helm rollback - pre-delete / post-delete — runs before and after resource cleanup
- test — runs when
helm testis invoked, useful for integration checks
The key insight is that hooks are separate from the main release. They execute at defined points, and Helm waits for them to complete before proceeding. If a hook fails, the release itself fails.
Pattern 1: Database Migrations Before Upgrade
The most common hook use case: run a migration Job before a new application version goes live. Here’s a pre-upgrade hook that runs a Kubernetes Job to apply schema changes:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-migrate
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
metadata:
name: {{ .Release.Name }}-migrate
spec:
restartPolicy: Never
containers:
- name: migrator
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "up"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db
key: url
Three annotations do the work here. The helm.sh/hook annotation marks this as a pre-upgrade hook. The weight of -5 ensures it runs before other hooks at the same stage. The delete policy cleans up the Job after it succeeds, so failed migrations leave evidence behind while successful ones don’t clutter the cluster.
Pattern 2: Post-Install Smoke Test
After deploying a service, you want to verify it actually works before declaring success. A post-install hook can hit a health endpoint and fail the release if the service isn’t responding:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-smoke-test
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: smoke
image: curlimages/curl:8.12.0
command:
- /bin/sh
- -c
- |
curl -sf http://{{ .Release.Name }}:{{ .Values.service.port }}/healthz || exit 1
Notice that one resource implements two hooks (post-install,post-upgrade). This means the smoke test runs both on fresh installs and upgrades — no need to duplicate the manifest.
Hook Weights: Ordering Multiple Hooks
When multiple hooks fire at the same lifecycle stage, Helm sorts them by weight in ascending order. Negative weights run first, positive weights run last. Weights are strings, not integers:
-10— runs first (e.g., create a database backup)-5— runs second (e.g., apply migrations)0— default weight if not specified5— runs later (e.g., seed reference data)10— runs last (e.g., smoke test)
Always set explicit weights. Helm 3.2.0+ falls back to standard resource ordering for same-weight hooks, but relying on that is fragile. Explicit weights make the execution order unambiguous and debuggable.
Deletion Policies: Cleanup That Actually Works
Hooks create resources that are not tracked as part of the release. This means helm uninstall won’t clean them up. The helm.sh/hook-delete-policy annotation controls when hook resources get removed:
- before-hook-creation (default) — deletes the previous hook resource before launching a new one
- hook-succeeded — removes the resource after successful execution
- hook-failed — removes the resource if the hook failed
A common combination is before-hook-creation,hook-succeeded. This ensures stale hooks from previous runs are cleaned before new ones launch, and successful runs leave no trace. Deliberately omit hook-failed when you want failed Jobs to stick around for debugging.
Pattern 3: Pre-Delete Graceful Shutdown
Before a release is torn down, you might want to drain a queue, notify a monitoring system, or archive data. A pre-delete hook handles this:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-drain
annotations:
"helm.sh/hook": pre-delete
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: drainer
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./drain-queue"]
env:
- name: QUEUE_NAME
value: {{ .Values.queue.name | quote }}
Production Checklist
- Always use
--atomic --waitwith hooks in CI. The--waitflag ensures Helm blocks until resources reach a ready state, and--atomicrolls back automatically if anything — including hooks — fails. This combination gives you safe, idempotent CI deployments. - Set
--timeoutgenerously. Hooks that run migrations or backups can take time. The default 5-minute timeout may be too short for large datasets. Set it explicitly:helm upgrade --install --atomic --wait --timeout 15m. - Pin your hook images. A hook that uses
latesttags will break when the upstream image changes. Always pin to specific versions in hook container specs. - Use
helm templateto validate hooks locally. Before pushing to a cluster, render the chart withhelm templateand inspect the hook manifests. This catches template errors, missing values, and YAML structure issues without touching the cluster. - Remember that hook resources survive release deletion. If you need a hook-created resource to persist across releases, annotate it with
helm.sh/resource-policy: keep. Otherwise, use the TTL controller on Jobs or explicit deletion policies.
Wrapping Up
Helm hooks bridge the gap between “apply some YAML” and “safely manage a release lifecycle.” The annotation system is simple, but the patterns — migrations, smoke tests, graceful shutdowns, weighted ordering — are what make charts production-ready. Combined with --atomic --wait in CI, hooks give you deployment pipelines that fail fast, roll back automatically, and leave no orphaned resources behind.
Start by adding a single pre-upgrade migration hook to an existing chart. Once that’s working in CI with --atomic, layer in post-install smoke tests and weighted cleanup hooks. The investment pays off the first time a bad release rolls back cleanly because a hook caught the problem early.