Helm and packaging applications

Turn a directory of manifests into a versioned chart: templates, values, releases, upgrades, rollbacks and hooks.

Charts and releases

A Helm chart is a parameterised bundle of manifests. A release is one installed instance of that chart in a namespace, with its own values and its own revision history. The same chart installed twice under different names gives two independent applications.

storefront/
  Chart.yaml            # name, version, appVersion, dependencies
  values.yaml           # defaults for every template variable
  charts/               # vendored subcharts
  templates/
    _helpers.tpl        # named templates you can include
    NOTES.txt           # printed after a successful install
    deployment.yaml
    service.yaml
    ingress.yaml
    tests/test-connection.yaml
  .helmignore
helm create storefront
helm lint ./storefront
helm template storefront ./storefront --debug | less   # render locally, no cluster
helm install storefront ./storefront -n storefront --create-namespace
helm list -A
helm status storefront -n storefront
helm get values storefront -n storefront
helm get manifest storefront -n storefront      # what is actually deployed
  • Helm stores the release state in Secrets named sh.helm.release.v1.* in the release namespace. That is your revision history and your rollback capability.
  • Always run helm template or --dry-run before installing. Charts are code, and rendering is the only review that catches a broken value path.
  • A release name plus namespace is the identity. Installing the same release twice in one namespace fails rather than clobbering.
  • helm get manifest is the ground truth when something in the cluster does not match your values.

Templates and values

Templates are Go text templates with functions. Values come from values.yaml, from files you pass with -f, from --set, and from the parent chart when it is a subchart. Later sources win over earlier ones.

# templates/deployment.yaml (trimmed)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "storefront.fullname" . }}
  labels:
    {{- include "storefront.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "storefront.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "storefront.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
# values.yaml
replicaCount: 2
image:
  repository: ghcr.io/example/storefront
  tag: ""                     # empty means use .Chart.AppVersion
resources:
  requests:
    cpu: 100m
    memory: 128Mi
ingress:
  enabled: false
SourcePrecedenceTypical use
Chart values.yamlLowestSensible defaults that work out of the box
Parent chart valuesOverrides subchart defaultsOne app composing a database subchart
-f prod.yamlOverrides the chart defaultsPer-environment configuration, committed to Git
--set key=valueHighest, in order givenOne-off changes and CI overrides
--set-stringHighestValues that must not be coerced to a number or boolean
helm upgrade --install storefront ./storefront -n storefront \
  -f values-prod.yaml --set image.tag=1.4.0 --atomic --wait --timeout 5m

helm rollback storefront 3 -n storefront
helm history storefront -n storefront
  • --install makes the command idempotent, which is what a deployment pipeline wants.
  • --atomic rolls back automatically when the upgrade fails, and --wait makes "failed" mean the pods did not become Ready rather than the API accepted the object.
  • sha256sum of a rendered ConfigMap in a pod annotation forces a rollout when configuration changes. Without it, a config change is invisible to the Deployment.
  • Quote values that look numeric: an unquoted 1.27 becomes a float and the image tag renders as 1.27.0.

Dependencies, hooks and uninstall

# Chart.yaml
apiVersion: v2
name: storefront
version: 1.4.0          # the chart version
appVersion: "2.1.0"     # the application version
dependencies:
  - name: postgresql
    version: "15.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled
---
# templates/hooks/migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "storefront.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
HookRuns
pre-install / pre-upgradeBefore the main resources are created or updated
post-install / post-upgradeAfter they are Ready
pre-deleteBefore the release is removed, e.g. a final backup
testOn helm test, a smoke check you can run in CI
  • helm dependency update vendors subcharts into charts/; helm dependency build reproduces them from the lock file in CI.
  • A hook is a normal object with an annotation. Helm waits for it, so a migration Job that hangs will hang the upgrade until the timeout.
  • Hook resources are not part of the release manifest, so they are not rolled back with it.
  • helm uninstall deletes the objects it created, including PersistentVolumeClaims unless they carry helm.sh/resource-policy: keep.
  • Use library charts for shared helpers across your own charts instead of copy-pasting _helpers.tpl.
helm dependency update ./storefront
helm test storefront -n storefront
helm uninstall storefront -n storefront
helm get all storefront -n storefront > release-backup.yaml

# CI gate: fail the pipeline if the rendered output differs from what is live
helm template storefront ./storefront -f values-prod.yaml | kubectl diff -f -
⚠️
Helm will happily delete a PersistentVolumeClaim on uninstall, and there is no undo for that. Annotate stateful resources with helm.sh/resource-policy: keep, and verify what a chart creates before you uninstall it in an environment that matters.

FAQ

Which values override which?
Chart defaults are overridden by the parent chart, then by files passed with -f in the order given, then by --set in the order given. Print the result with helm get values --all rather than guessing.
Why does Helm say the release is in a failed state?
A previous upgrade did not complete: a hook or a pod never became Ready. Fix the underlying problem, then helm rollback to the last good revision, or helm uninstall if the release holds nothing of value.

kubectl fundamentals and writing manifests RBAC, service accounts and pod security

Last refreshed 2026-09-18.