Helm Hooks
Run jobs at specific points in a release lifecycle. Use hooks for database migrations, backups, and cleanup tasks.
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
| Hook | When It Runs |
|---|---|
pre-install | After templates render, before any resources are created |
post-install | After all resources are loaded into Kubernetes |
pre-delete | Before any resources are deleted from Kubernetes |
post-delete | After all resources have been deleted |
pre-upgrade | After templates render, before any resources are updated |
post-upgrade | After all resources have been upgraded |
pre-rollback | Before a rollback is executed |
post-rollback | After a rollback is executed |
test | When 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:
- Backup database (weight 0)
- Run migrations (weight 5)
- 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
| Policy | Behavior |
|---|---|
hook-succeeded | Delete the hook resource after it succeeds |
hook-failed | Delete the hook resource if it fails |
before-hook-creation | Delete 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-upgradehook Job fails, the upgrade doesn't proceed. - The release is marked as
failed. You can see this withhelm list. - Resources may be in a partial state.
post-installhooks 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?"
| Consideration | Hook | Init Container |
|---|---|---|
| Runs once per release | Yes | No (runs per pod) |
| Blocks deployment | Yes (pre-* hooks) | No (blocks individual pod) |
| Can exist independently | Yes (separate Job) | No (part of pod spec) |
| Failure handling | Fails the release | Pod enters CrashLoopBackOff |
| Visibility | kubectl get jobs | kubectl 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.