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
.helmignorehelm 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 templateor--dry-runbefore 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 manifestis 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| Source | Precedence | Typical use |
|---|---|---|
Chart values.yaml | Lowest | Sensible defaults that work out of the box |
| Parent chart values | Overrides subchart defaults | One app composing a database subchart |
-f prod.yaml | Overrides the chart defaults | Per-environment configuration, committed to Git |
--set key=value | Highest, in order given | One-off changes and CI overrides |
--set-string | Highest | Values 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--installmakes the command idempotent, which is what a deployment pipeline wants.--atomicrolls back automatically when the upgrade fails, and--waitmakes "failed" mean the pods did not become Ready rather than the API accepted the object.sha256sumof 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.27becomes a float and the image tag renders as1.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| Hook | Runs |
|---|---|
pre-install / pre-upgrade | Before the main resources are created or updated |
post-install / post-upgrade | After they are Ready |
pre-delete | Before the release is removed, e.g. a final backup |
test | On helm test, a smoke check you can run in CI |
helm dependency updatevendors subcharts intocharts/;helm dependency buildreproduces 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 uninstalldeletes the objects it created, including PersistentVolumeClaims unless they carryhelm.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.Related
kubectl fundamentals and writing manifests RBAC, service accounts and pod security
Last refreshed 2026-09-18.