Helm hooks let you run custom logic before or after a Helm release is installed, upgraded, or deleted.

Here’s a deployment that uses a Helm hook to run a pre-install script:

apiVersion: batch/v1
kind: Job
metadata:
  name: my-pre-install-job
  annotations:
    "helm.sh/hook": pre-install
    "helm.sh/hook-weight": "1"
spec:
  template:
    spec:
      containers:
      - name: pre-install-container
        image: alpine:latest
        command: ["/bin/sh", "-c"]
        args:
        - echo "Running pre-install script...";
          # Example: Check if a resource already exists

          if kubectl get configmap my-app-config -n {{ .Release.Namespace }} > /dev/null 2>&1; then

            echo "ConfigMap 'my-app-config' already exists. Skipping creation.";
          else
            echo "Creating ConfigMap 'my-app-config'...";

            kubectl create configmap my-app-config --from-literal=setting=initial_value -n {{ .Release.Namespace }};

            echo "ConfigMap created.";
          fi;
          echo "Pre-install script finished."
      restartPolicy: Never
  backoffLimit: 4

This Job is configured to run before the main release resources are installed, thanks to the helm.sh/hook: pre-install annotation. The hook-weight of 1 means it will run after any other hooks with a lower weight (e.g., 0) and before any with a higher weight.

The core problem Helm hooks solve is managing complex deployment lifecycles where certain actions must happen at specific points. For example, you might need to:

  • Pre-populate a database: Run a migration script before your application Pods start.
  • Create or update configurations: Ensure a ConfigMap or Secret exists with the correct data before the application consumes it.
  • Perform health checks: Verify external dependencies are available.
  • Clean up old resources: Remove stale Jobs or Deployments from a previous release.
  • Seed initial data: Populate a cache or a simple data store.

The helm.sh/hook annotation is the key. It tells Helm to treat this Kubernetes resource (like a Job, Pod, or ConfigMap) as a hook. The possible values are:

  • pre-install: Runs before any release resources are created.
  • post-install: Runs after all release resources are created.
  • pre-delete: Runs before any release resources are deleted.
  • post-delete: Runs after all release resources are deleted.
  • pre-upgrade: Runs before any release resources are upgraded.
  • post-upgrade: Runs after all release resources are upgraded.
  • pre-rollback: Runs before any release resources are rolled back.
  • post-rollback: Runs after any release resources are rolled back.

These hooks are typically implemented as Jobs or Pods. When Helm encounters a hook, it creates the specified resource, waits for it to complete (for Jobs and Pods that terminate), and then proceeds with the main Helm operation. If a hook fails, the Helm operation can be configured to fail as well, preventing a broken deployment.

The helm.sh/hook-delete-policy annotation controls whether the hook resource itself is deleted after it has served its purpose. Common values are hook-succeeded (delete the hook resource if it completes successfully) and hook-failed (delete the hook resource if it fails). If omitted, the hook resource typically remains in the cluster.

Let’s look at a post-install hook example, which might be used to notify a Slack channel about a new deployment:

apiVersion: batch/v1
kind: Job
metadata:
  name: my-post-install-notifier
  annotations:
    "helm.sh/hook": post-install
    "helm.sh/hook-weight": "10"
    "helm.sh/hook-delete-policy": "hook-succeeded"
spec:
  template:
    spec:
      containers:
      - name: notifier-container
        image: curlimages/curl:latest
        command: ["/bin/sh", "-c"]
        args:
        - |
          echo "Sending Slack notification...";
          curl -X POST -H 'Content-type: application/json' \

          --data '{"text":"Deployment of release '\''{{ .Release.Name }}'\'' in namespace '\''{{ .Release.Namespace }}'\'' completed successfully!"}' \


          {{ .Values.slackWebhookUrl }};

          echo "Notification sent."
      restartPolicy: Never
  backoffLimit: 1

Here, the post-install hook uses curlimages/curl to send a message to a Slack webhook. The hook-delete-policy: hook-succeeded ensures this Job is cleaned up automatically once it finishes without errors. The hook-weight: 10 ensures it runs after the main application resources are deployed.

A common point of confusion is how Helm manages the lifecycle of these hook resources. Helm doesn’t execute the script within the hook resource itself; rather, it creates the Kubernetes resource (like a Job or Pod) that contains the script. Helm then monitors the lifecycle of that Kubernetes resource. For Jobs and Pods that are designed to run and terminate, Helm waits for their Completed or Failed status. If a hook resource fails to start or complete as expected (e.g., the Pod never becomes Ready or the Job hits its backoffLimit), Helm will typically mark the release operation as failed.

Another subtle but powerful aspect is that you can use any Kubernetes resource as a hook, not just Jobs. A ConfigMap hook, for instance, could be used to inject configuration data that another part of the system (perhaps a controller you’ve also deployed) will react to. However, for actions that need to be explicitly run and completed, Jobs are the most common and straightforward choice.

The next step when working with Helm hooks is to manage their dependencies and ordering more precisely, often using multiple hooks with different hook-weight values or even employing a dedicated service that your hooks interact with.

Want structured learning?

Take the full Helm course →