Helm Hooks vs Init Containers: Where Ordering-Sensitive Work Belongs
Helm hooks buy ordering but cost visibility: they vanish from helm template and diff tooling. A decision rule for migrations, warm-ups, and setup jobs.
09 Oct 2025, 15:58 UTC

The migration that runs at the wrong time
You add a schema migration to a Helm chart. It must run once, finish before the new application pods start, and never run twice concurrently. The obvious move is a Job in the chart. But a plain Job in the main manifest has no ordering guarantee against the Deployment — Helm sorts resources by kind and name, not by what your application needs.
Helm hooks solve this by pulling a resource out of the main manifest and running it at a named point in the release lifecycle. That is a real capability, and it is also a real cost. The short version: use hooks for short, idempotent, ordering-sensitive work; reach for init containers or a pipeline step when the work needs to be observable, retried on your terms, or reviewed in a diff.
What the hook annotation actually changes
A hook is an ordinary Kubernetes resource with an annotation such as helm.sh/hook: pre-upgrade. Recognized phases include pre-install, post-install, pre-upgrade, post-upgrade, pre-rollback, post-rollback, pre-delete, post-delete, and test. Instead of being applied with the rest of the rendered chart, the resource is applied when Helm reaches that phase.
Two annotations do most of the day-to-day work:
helm.sh/hook-weightorders resources within a phase. Lower values run first; negative values are allowed. Ties fall back to a deterministic sort, so if two hooks must not race, set explicit weights rather than relying on that fallback.helm.sh/hook-delete-policycontrols cleanup, with valuesbefore-hook-creation,hook-succeeded, andhook-failed. In Helm 3, a successful hook resource is not deleted by default.before-hook-creationis the usual fix when a second upgrade fails because the previous hookJobis still sitting there.
This behavior is version-sensitive. Helm 2 differs in annotation names and default deletion behavior, so confirm which major version your tooling uses before generalizing any example here.
The visibility tax
Because hook resources are tracked separately from the release's main manifest, they disappear from the places engineers normally look:
helm templatedoes not render them. A reviewer reading rendered output will not see the migration that is about to run.- Diff tooling built on the main manifest can silently miss hook changes — which are often the riskiest part of an upgrade.
helm uninstalldoes not remove hook resources, soJobs,Pods, and logs can accumulate.- Hooks only execute against a live cluster.
--dry-rundoes not meaningfully simulate the lifecycle.
That last point is the sharpest trade-off: the changes with the highest blast radius are the ones your local rendering and review workflow exercises least.
A worked example: a pre-upgrade migration
Run this against a test cluster. The chart below assumes a Helm 3 client and a cluster where the release's service account may create Jobs in the target namespace.
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": before-hook-creation
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "--to-latest"]
Each choice matters. The negative weight runs this before any other pre-upgrade hook. before-hook-creation clears the previous Job so a repeat upgrade does not collide with it. activeDeadlineSeconds and backoffLimit are set deliberately because a hook Job that never completes blocks the entire release until Helm's timeout — so pass an explicit --timeout rather than accepting the default.
To check the visibility claim yourself, render the chart and confirm the hook is absent from the output:
helm template ./chart | grep -i "helm.sh/hook"
# expect no matches
Then install or upgrade against a test cluster and inspect the release. Whether your client exposes a hook-listing subcommand depends on the version — check helm get --help before relying on one. You can also look at the release storage backend (commonly Secrets labeled owner=helm) to see hook records stored separately from the main manifest.
The trade-off, and how to choose
Use a hook when the work is short, idempotent, and genuinely ordering-sensitive: a schema migration, a cache warm, an index build. Prefer an init container or an explicit CI pipeline step when the work must be observable, retried under your own policy, tested outside Helm, or cleaned up strictly. Hooks run with the same cluster credentials as the release, which makes them a poor home for one-off secret handling.
If you use --atomic (which implies --wait), a failing hook rolls the release back. That does not undo side effects the hook already performed. Treat migrations as backward-compatible and safe to re-run, and verify by running the same upgrade twice: first without a delete policy to reproduce the leftover-Job failure, then with before-hook-creation to confirm the fix.
What to do next
Audit one chart that uses hooks. Render it, confirm the hook resources are missing from helm template, and decide whether that invisibility is acceptable for what the hook does. If the answer is no, move the work into an init container or a pipeline step where it shows up in review and in your normal logs. If the answer is yes, add explicit weights and a delete policy so the behavior is intentional rather than incidental.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.