Helm Hooks

Run jobs at specific points in a release lifecycle. Use hooks for database migrations, backups, and cleanup tasks.

7 min read

Helm Hooks

In the previous tutorial, we composed charts with dependencies. But sometimes you need things to happen at very specific moments — run a database migration before an upgrade, back up data before a delete, or send a notification after an install. That's what hooks are for.

What Are Hooks?

Hooks are regular Kubernetes resources (usually Jobs) with special annotations that tell Helm when to create them. They're not part of the normal release — they run at specific lifecycle events.

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-db-migrate
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: my-app:{{ .Chart.AppVersion }}
          command: ["python", "manage.py", "migrate"]
      restartPolicy: Never
  backoffLimit: 3

That annotation — "helm.sh/hook": pre-upgrade — is what makes this a hook instead of a regular resource.

Hook Types

HookWhen It Runs
pre-installAfter templates render, before any resources are created
post-installAfter all resources are loaded into Kubernetes
pre-deleteBefore any resources are deleted from Kubernetes
post-deleteAfter all resources have been deleted
pre-upgradeAfter templates render, before any resources are updated
post-upgradeAfter all resources have been upgraded
pre-rollbackBefore a rollback is executed
post-rollbackAfter a rollback is executed
testWhen helm test is called (covered in the testing tutorial)

You can combine multiple hooks on one resource:

annotations:
  "helm.sh/hook": pre-install,pre-upgrade

This migration Job runs on both fresh installs and upgrades.

The Release Lifecycle

Here's the exact order of events during helm install:

1. User runs: helm install my-release ./my-chart
2. Helm renders all templates
3. Helm sorts resources by kind (Namespaces first, then ServiceAccounts, etc.)
4. pre-install hooks run (wait for completion)
5. All chart resources are created in Kubernetes
6. post-install hooks run (wait for completion)
7. Release is marked as "deployed"

For helm upgrade:

1. User runs: helm upgrade my-release ./my-chart
2. Helm renders all templates
3. pre-upgrade hooks run (wait for completion)
4. Resources are updated (3-way strategic merge)
5. post-upgrade hooks run (wait for completion)
6. Release revision is incremented

Hook Weight — Ordering Hooks

When you have multiple hooks of the same type, hook-weight controls the order (ascending):

# Runs first (weight 0)
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-backup
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "0"
spec:
  template:
    spec:
      containers:
        - name: backup
          image: backup-tool:latest
          command: ["backup", "--database", "mydb"]
      restartPolicy: Never
  backoffLimit: 1
---
# Runs second (weight 5)
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-migrate
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "5"
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: my-app:{{ .Chart.AppVersion }}
          command: ["python", "manage.py", "migrate"]
      restartPolicy: Never
  backoffLimit: 3

Execution order:

  1. Backup database (weight 0)
  2. Run migrations (weight 5)
  3. Upgrade the application

Weights are strings (YAML requires quoting numbers in annotations) and sorted in ascending order. Hooks with the same weight are sorted by resource kind and name.

Hook Delete Policies

Hooks create real Kubernetes resources. What happens to them after they run?

annotations:
  "helm.sh/hook": post-install
  "helm.sh/hook-delete-policy": hook-succeeded
PolicyBehavior
hook-succeededDelete the hook resource after it succeeds
hook-failedDelete the hook resource if it fails
before-hook-creationDelete any existing hook resource before creating a new one

You can combine policies:

# Delete on success, also clean up old hooks before creating new ones
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation

"What if I don't set a delete policy?"

The hook resource stays around forever (or until you delete it manually, or delete the release). This can cause problems — the next helm upgrade might fail because the Job already exists. That's why before-hook-creation is a common default.

Common Hook Patterns

Database Migration (pre-upgrade)

The classic use case:

{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-chart.fullname" . }}-migrate
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    metadata:
      labels:
        {{- include "my-chart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: {{ toYaml .Values.migrations.command | nindent 12 }}
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ include "my-chart.fullname" . }}-db-secret
                  key: url
      restartPolicy: Never
  backoffLimit: {{ .Values.migrations.backoffLimit | default 3 }}
  activeDeadlineSeconds: {{ .Values.migrations.timeout | default 300 }}
{{- end }}
# values.yaml
migrations:
  enabled: true
  command: ["python", "manage.py", "migrate", "--noinput"]
  backoffLimit: 3
  timeout: 300

Data Backup (pre-delete)

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-final-backup
  annotations:
    "helm.sh/hook": pre-delete
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  template:
    spec:
      containers:
        - name: backup
          image: backup-tool:latest
          command:
            - /bin/sh
            - -c
            - |
              pg_dump $DATABASE_URL > /backups/final-$(date +%Y%m%d).sql
              aws s3 cp /backups/ s3://my-bucket/backups/ --recursive
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ .Release.Name }}-db-secret
                  key: url
      restartPolicy: Never
  backoffLimit: 1

Initialization ConfigMap (pre-install)

Not all hooks need to be Jobs. Any resource type works:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-init-config
  annotations:
    "helm.sh/hook": pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  init.sql: |
    CREATE DATABASE IF NOT EXISTS myapp;
    CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'password';
    GRANT ALL ON myapp.* TO 'appuser'@'%';

Notification (post-install, post-upgrade)

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-notify
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "10"
    "helm.sh/hook-delete-policy": hook-succeeded,hook-failed
spec:
  template:
    spec:
      containers:
        - name: notify
          image: curlimages/curl:latest
          command:
            - /bin/sh
            - -c
            - |
              curl -X POST "$SLACK_WEBHOOK" \
                -H 'Content-type: application/json' \
                -d '{"text":"Release {{ .Release.Name }} v{{ .Chart.AppVersion }} deployed to {{ .Release.Namespace }}"}'
          env:
            - name: SLACK_WEBHOOK
              valueFrom:
                secretKeyRef:
                  name: {{ .Release.Name }}-slack
                  key: webhook-url
      restartPolicy: Never
  backoffLimit: 1

Hook Failures

What happens when a hook fails?

  • The release is blocked. If a pre-upgrade hook Job fails, the upgrade doesn't proceed.
  • The release is marked as failed. You can see this with helm list.
  • Resources may be in a partial state. post-install hooks failing doesn't roll back the install.

To set a timeout for hooks:

# Wait up to 10 minutes for hooks to complete
helm install my-release ./my-chart --timeout 10m

The --timeout flag applies to the entire install/upgrade process, including hooks.

Debugging Failed Hooks

# Check hook Job status
kubectl get jobs -l app.kubernetes.io/instance=my-release

# Check pod logs
kubectl logs job/my-release-migrate

# Describe for events
kubectl describe job/my-release-migrate

Hooks vs Init Containers

"Should I use a hook or an init container for my migration?"

ConsiderationHookInit Container
Runs once per releaseYesNo (runs per pod)
Blocks deploymentYes (pre-* hooks)No (blocks individual pod)
Can exist independentlyYes (separate Job)No (part of pod spec)
Failure handlingFails the releasePod enters CrashLoopBackOff
Visibilitykubectl get jobskubectl get pods

Use hooks for one-time operations like migrations, data seeding, or notifications. Use init containers for per-pod setup like waiting for a dependency to be ready.

What's Next?

Hooks give you precise control over the release lifecycle. Combined with weights and delete policies, you can orchestrate complex deployment workflows.

In the next tutorial, we'll cover managing releases and rollbacks — how to upgrade, rollback, and inspect releases in production.