Helm
Helm is the de facto package manager for Kubernetes — the tool that bundles a set of related Kubernetes manifests into a single, versioned, parameterized, distributable unit called a chart, and manages the lifecycle of each installed instance of that chart, called a release, in a cluster (helm.sh — Using Helm). The documentation’s own analogy is the most useful framing: a chart is to Kubernetes what a Homebrew formula or an RPM package is to an operating system, and the chart repository (or, increasingly, an OCI registry) is the package index. Helm is a CNCF graduated project — accepted into the Cloud Native Computing Foundation as an incubating project on 1 June 2018 and promoted to graduated maturity on 1 May 2020, the tenth project to reach that tier (CNCF — Helm graduation, 2020; CNCF — Helm project page). Note that “graduated” is a CNCF governance/maturity status, not a version — it is routinely conflated with release maturity, which is a separate axis (Helm’s current release line is Helm 4, covered below). Helm solves three problems at once that raw
kubectl applydoes not: packaging (onehelm installbrings up a Deployment + Service + Ingress + ConfigMap + HPA + ServiceAccount + RBAC as an atomic, named unit), parameterization (the same chart deploys to dev, staging, and prod by overriding values), and lifecycle management (every install/upgrade/rollback is a numbered revision with full history, so a bad upgrade is onehelm rollbackaway). This note covers Helm as a system — its three core nouns, its CLI verbs, the Helm 2 → 3 → 4 evolution, distribution mechanics, and hooks. The internals of a chart’s directory layout live in the sibling Helm Chart Anatomy; the perennial comparison with the template-free alternative lives in Helm vs Kustomize.
Mental Model
flowchart TB subgraph AUTHOR["Chart author side"] CHART["Chart<br/>(Chart.yaml + templates/ + values.yaml)<br/>versioned, packaged as .tgz"] REPO[("Chart repository<br/>or OCI registry")] CHART -- "helm package + helm push" --> REPO end subgraph USER["Chart consumer side"] VALUES["values override<br/>(-f prod.yaml / --set)"] HELM["helm CLI<br/>(client-only since Helm 3)"] REPO -- "helm pull / helm install oci://" --> HELM VALUES --> HELM end subgraph CLUSTER["Cluster"] APISERVER[(kube-apiserver)] REL_SECRET["Release record<br/>Secret sh.helm.release.v1.NAME.vN<br/>in the release namespace"] OBJECTS["Live objects<br/>Deployment, Service, ConfigMap, ..."] end HELM -- "render templates locally<br/>then apply manifests" --> APISERVER APISERVER --> OBJECTS APISERVER --> REL_SECRET REL_SECRET -. "history of revisions<br/>enables helm rollback" .-> HELM
What this diagram shows. Helm has two sides. On the author side, a chart is developed, version-bumped, packaged into a .tgz, and pushed to a repository or OCI registry. On the consumer side, the helm CLI pulls the chart, merges the chart’s default values.yaml with the user’s overrides, renders the Go templates locally into plain Kubernetes manifests, and applies those manifests to the API server. The insight to extract — and the single most common misconception about Helm — is that Helm 3+ has no in-cluster server component. The rendering and merging happen entirely in the client. The only thing Helm stores in the cluster is the release record: a Secret (named sh.helm.release.v1.<release>.v<revision>) in the release’s namespace holding a gzip-compressed snapshot of the rendered manifests and metadata for that revision. That Secret is what makes helm rollback, helm history, and helm get manifest possible — and its per-namespace, per-revision shape is exactly the design that the removal of Tiller (covered below) made possible.
Mechanical Walk-through
The three nouns: chart, release, values
A chart is the package: a directory (or .tgz archive) containing a Chart.yaml metadata file, a values.yaml of default parameters, and a templates/ directory of Go-templated Kubernetes manifests. Charts are versioned twice over — the chart’s own version (semver, bumped whenever the packaging changes) and the appVersion (the version of the application inside, purely informational). The full directory layout is the subject of Helm Chart Anatomy.
A release is an installed instance of a chart in a cluster. The chart-to-release relationship is one-to-many: helm install web-prod ./mychart and helm install web-staging ./mychart create two independent releases from the same chart, each with its own name, namespace, value overrides, and revision history. Naming the release explicitly is the user’s job; --generate-name lets Helm pick one.
Values are the parameterization layer. The chart ships values.yaml with defaults; the user overrides them at install/upgrade time with -f myvalues.yaml (one or more files, last-wins) and --set key=value (highest precedence, supports nested paths, list indexing, and --set-string/--set-file/--set-json variants). The merged result becomes the .Values object the templates render against.
The CLI verbs
helm install web ./mychart -f prod.yaml # create release "web" from a chart
helm upgrade web ./mychart -f prod.yaml # apply a new chart/values version to "web"
helm upgrade --install web ./mychart # idempotent: install if absent, upgrade if present
helm rollback web 3 # revert "web" to revision 3 (a numbered, stored snapshot)
helm uninstall web # remove the release and its objects
helm list # list releases in the current namespace
helm list --all-namespaces # cluster-wide
helm history web # show the revision history of "web"
helm template ./mychart -f prod.yaml # render templates to stdout WITHOUT touching the cluster
helm get manifest web # dump the rendered manifests of the live release
helm status web # show release status + NOTES.txt output
helm diff upgrade web ./mychart # (helm-diff plugin) preview an upgradehelm upgrade is minimally invasive — the documentation states it “will only update things that have changed since the last release” (helm.sh — Using Helm). helm upgrade --install is the workhorse of CI pipelines: it makes the operation idempotent so a pipeline need not branch on “does this release exist yet.” helm template is the pure-function escape hatch — it renders without a cluster connection, which is what makes Helm composable with Kustomize and with CI lint stages (see Helm vs Kustomize).
How an upgrade reconciles: three-way merge vs Server-Side Apply
When helm upgrade runs, Helm must reconcile the newly rendered manifests against what is live in the cluster — without clobbering fields written by controllers (an HPA’s spec.replicas, a cloud LB’s status). Helm 3 does this with a three-way strategic merge patch: it diffs (a) the manifests from the previous release revision, (b) the manifests it is rendering now, and (c) the live object — and computes a patch that applies the author’s intended changes while leaving fields Helm never managed untouched. This is conceptually the same algorithm as kubectl apply’s client-side three-way merge (see Declarative vs Imperative Configuration), with the “last-applied” baseline supplied by the stored release record rather than an annotation.
Helm 4 — released 12 November 2025 at KubeCon + CloudNativeCon North America in Atlanta, coinciding with Helm’s tenth anniversary and the first major version in six years (CNCF — Helm 4 announcement; helm.sh — Helm 4 released) — changes the default reconciliation strategy: new installations use Server-Side Apply (SSA), where the API server (not the client) tracks per-field ownership in metadata.managedFields (kubernetes.io — SSA GA, stable since K8s 1.22). The qualifier “new installations” is load-bearing. Per the Helm 4 write-up, SSA is the default only for new installs; “for upgrades and rollbacks, Helm retains the method used during the initial installation: all your existing releases created with Helm 3 will therefore continue to use client-side apply after migrating to Helm 4” — you must pass --server-side explicitly to switch an existing release (enix.io — Helm 4, tracking HIP-0023). Helm 4 also introduces a --force-conflicts=false|true flag (defaulting to false), mirroring kubectl, on the install, upgrade, and rollback commands for the SSA field-ownership-conflict case. This is the inverse of a common misconception: --server-side is not a Helm 3 option — Helm 3’s upgrade reconciliation is exclusively the client-side three-way strategic merge described above. When reading any “Helm does SSA” claim, pin which major version the cluster runs.
Two other Helm 4 changes are worth noting because they touch this reconciliation path. First, the wait/readiness machinery is rebuilt on kstatus — the kubernetes-sigs library that computes a uniform Reconciling/Current/Failed status across arbitrary resource kinds — giving --wait far finer-grained readiness detection than Helm 3’s per-kind heuristics. Second, the plugin system was redesigned around an optional WebAssembly (Wasm) runtime, so plugins (including post-renderers and getters) can run as sandboxed Wasm modules rather than arbitrary host binaries (helm.sh — Helm 4 released; enix.io — Helm 4). Helm 3 charts (chart API v2) continue to work unmodified under Helm 4, and the Helm maintainers committed Helm 3 to bug fixes until 8 July 2026 and security fixes until 11 November 2026, with no feature backports beyond Kubernetes client-library updates (helm.sh — Helm 4 released).
Hooks
Helm charts can ship hooks — ordinary manifests annotated with helm.sh/hook that Helm runs at specific lifecycle points rather than as part of the normal apply (helm.sh — Chart Hooks). The nine hook points are pre-install, post-install, pre-upgrade, post-upgrade, pre-rollback, post-rollback, pre-delete, post-delete, and test. A pre-install hook “executes after templates are rendered, but before any resources are created” — the canonical use is a Job that runs a database schema migration before the new application Pods come up. Hooks are ordered with helm.sh/hook-weight (ascending integer, ties broken by kind) and cleaned up per helm.sh/hook-delete-policy (before-hook-creation — the default, hook-succeeded, hook-failed). If a hook is a Job or Pod, Helm waits for it to run to completion before proceeding.
Configuration / API Surface
A minimal helm install walk-through with the release record it produces:
# 1. Add a classic chart repository (an index.yaml + .tgz files over HTTP)
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# 2. Inspect the chart's parameters before committing
helm show values bitnami/nginx > /tmp/nginx-values.yaml
# 3. Install — naming the release "edge" into namespace "web"
helm install edge bitnami/nginx \
--namespace web --create-namespace \
-f /tmp/nginx-values.yaml \
--set replicaCount=4 \
--wait --timeout 5m
# --wait : block until Pods are Ready and Services have an address
# --timeout : ceiling on the wait (default 5m0s)
# 4. The cluster now holds a release-record Secret:
kubectl -n web get secret -l owner=helm
# NAME TYPE DATA
# sh.helm.release.v1.edge.v1 helm.sh/release.v1 1
# ^ revision 1; an upgrade adds ...v2, ...v3, each a stored snapshot
# 5. Upgrade — bumps the release to revision 2
helm upgrade edge bitnami/nginx --namespace web --set replicaCount=6
# 6. A bad upgrade is one command away from undone
helm rollback edge 1 --namespace web # restore the revision-1 snapshot
# 7. OCI registry distribution (the modern alternative to chart repos)
helm registry login registry.example.com -u ci
helm push mychart-1.4.0.tgz oci://registry.example.com/charts
helm install edge oci://registry.example.com/charts/mychart --version 1.4.0Line-by-line points worth internalizing:
--create-namespace— Helm will not create the target namespace unless told to; a missing namespace is a common first-install failure.- The release-record Secret is the entire server-side footprint of Helm 3+. It lives in the release namespace (not a central namespace), is labelled
owner=helm, and is namedsh.helm.release.v1.<release>.v<revision>. Deleting it by hand orphans the release from Helm’s point of view. oci://charts skip thehelm repo addstep entirely; the registry is the index. OCI-registry distribution graduated to a stable, default-on feature in Helm 3.8 (early 2022) and is the recommended path going forward — any OCI-compliant registry (Harbor, ECR, GHCR, Artifactory, Docker Hub) can host charts alongside images.
Failure Modes
-
UPGRADE FAILED: another operation is in progress. A previoushelminvocation crashed mid-operation, leaving the release in apending-install/pending-upgradestatus. Diagnose withhelm history; recover withhelm rollbackto the last good revision, or in stubborn cases delete the stuckpending-*release-record Secret. -
Three-way-merge drift fights with a GitOps controller. If Helm 3 manages a release and ArgoCD/Flux also applies the same objects, the two reconcilers can ping-pong on fields neither cleanly owns. This is precisely the problem Server-Side Apply field ownership solves — and the reason Helm 4 adopted SSA. Mitigation pre-Helm-4: let exactly one tool own each object, or use ArgoCD’s Helm-template-only mode.
-
helm rollbackdoes not roll back data. Rollback restores the manifest snapshot of a prior revision; it does nothing for a schema migration apre-upgradehook already ran against a database. Migrations must be designed to be backward-compatible, or rollback will leave the app pointed at a schema it can’t read. -
The 1 MiB release-record limit. The release Secret holds a gzip-compressed copy of all rendered manifests. Charts that render hundreds of large objects (or embed big files via
.Files) can blow past etcd’s ~1.5 MiB value ceiling, and the install fails. Mitigation: split the chart, or store the release record in a SQL backend (HELM_DRIVER=sql). -
helm templateoutput diverges fromhelm installoutput.helm templatedoes not contact the cluster, so.Capabilities.APIVersionsand.Capabilities.KubeVersionfall back to client defaults. A chart that branches on.Capabilitieswill render differently offline than on-cluster. Mitigation: pass--kube-versionand--api-versionstohelm template.
Alternatives and When to Choose Them
- Kustomize — template-free, overlay-based customization built into
kubectl. No packaging or release lifecycle; the right tool when your manifests are yours and vary by structured patching rather than parameter substitution. See Helm vs Kustomize for the full comparison. - Raw
kubectl apply -f— no packaging, no parameterization, no rollback-by-revision. Fine for a handful of static manifests; collapses under YAML Sprawl Anti-Pattern at scale. - Jsonnet /
kubecfg/ Tanka, Pulumi, CDK8s — programmatic manifest generation in a real language. More expressive than Go templates, less of an ecosystem than Helm’s public chart catalog. - Operators — for stateful applications whose lifecycle is more than install/upgrade (backups, failover, scaling logic), a Helm chart is insufficient; an Operator Pattern controller is the right abstraction. Helm and operators compose: many operators are installed by a Helm chart.
The honest positioning: Helm wins decisively for distributing third-party software (the public chart ecosystem on Artifact Hub is enormous) and for release lifecycle (numbered revisions, one-command rollback). It is most criticized for template-soup readability — Go templates producing YAML by string substitution, where a misplaced indent is a runtime YAML parse error.
Production Notes
- Spotify’s Backstage and most Spotify microservices ship as Helm-managed Deployments, typically Deployment + Service + HPA + PodDisruptionBudget generated by a per-service chart — see the Production Notes in Deployment.
- The Helm 2 → 3 migration (Tiller removal, 2019) was an industry-wide event; the
helm-2to3plugin migrated release records from Tiller’s central namespace to per-namespace Secrets. The Helm 3 → 4 migration (SSA, November 2025) is comparatively gentle because Helm 4 keeps existing releases on their original merge strategy and leaves chart-API-v2charts unmodified. - GitOps with Helm is overwhelmingly done by rendering the chart and committing or reconciling the output, rather than letting a CI job run
helm installwith cluster credentials. Flux has a dedicatedHelmReleaseCRD and helm-controller; ArgoCD treats a chart as a templating source and applies the rendered manifests itself. Both are covered in GitOps, Flux, and ArgoCD.
See Also
- Helm Chart Anatomy — the internal directory layout and template language of a chart
- Kustomize — the template-free alternative
- Helm vs Kustomize — the recurring comparison and the “use both” reality
- GitOps — how Helm fits into pull-based continuous reconciliation
- ArgoCD / Flux — GitOps engines that consume Helm charts
- Declarative vs Imperative Configuration — the three-way merge Helm 3 shares with
kubectl apply - Server-Side Apply — the field-ownership model Helm 4 adopts
- Custom Resource Definition — what a chart’s
crds/directory installs - YAML Sprawl Anti-Pattern — the problem Helm exists to fix
- Kubernetes MOC — umbrella index (§15 Application Lifecycle and Delivery)