Helm Chart Hooks: Injecting Lifecycle Jobs into Kubernetes Deployments

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 test is 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 specified
  • 5 — 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 --wait with hooks in CI. The --wait flag ensures Helm blocks until resources reach a ready state, and --atomic rolls back automatically if anything — including hooks — fails. This combination gives you safe, idempotent CI deployments.
  • Set --timeout generously. 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 latest tags will break when the upstream image changes. Always pin to specific versions in hook container specs.
  • Use helm template to validate hooks locally. Before pushing to a cluster, render the chart with helm template and 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.

Leave a Reply

Your email address will not be published. Required fields are marked *