Helm
The package manager for Kubernetes: templated charts, values per environment, releases you can roll back.
In 30 seconds
Helm packages Kubernetes applications as charts: folders of templated manifests plus a values file with sensible defaults. You install a chart into a cluster as a release, override values per environment, upgrade it with new values or versions, and roll back to any earlier revision with one command. It is the standard way to install third-party software on Kubernetes and a common way to deploy your own services.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- Kubernetes manifests for one app quickly reach hundreds of lines across Deployments, Services, Ingresses and ConfigMaps; Helm turns them into one parameterised package.
- The same chart deploys to dev, staging and production with different values files, instead of copied YAML that drifts apart.
- Releases are versioned in the cluster, so upgrades are tracked and rollback to a known-good revision is one command.
- Most open-source infrastructure (ingress controllers, cert-manager, Prometheus, databases) ships an official Helm chart, so Helm literacy is expected of anyone running Kubernetes.
How it works
01Charts, values and releases
Three words carry the whole idea. A chart is a package: a folder with Chart.yaml (name, version, dependencies), values.yaml (default settings) and a templates/ directory of Kubernetes manifests written with Go templates. Values are the inputs: image tag, replica count, resource limits, hostnames. A release is one installed instance of a chart in a cluster, with a name and a namespace; you can install the same chart several times as different releases. Think of a chart as a recipe with blanks, values as your answers to the blanks, and a release as the dish actually served, with a numbered history of every time you changed it. Helm renders templates with values into plain manifests, sends them to the Kubernetes API, and records the release revision as a Secret in the release's namespace. There is no server-side component to install.
02Installing and managing third-party charts
Charts are distributed through OCI registries (the same registries that hold container images) or classic HTTP chart repositories. The commands follow a simple lifecycle. helm install creates a release; helm upgrade --install is idempotent and is what pipelines use; helm list shows releases; helm history shows revisions; helm rollback returns to a previous revision; helm uninstall removes everything the release created. Always inspect the chart's default values before installing, and pass only your overrides in your own file. Pin the chart version with --version so a new upstream release does not surprise you. --wait makes Helm wait until resources are ready, and with Helm 4 --rollback-on-failure (the renamed --atomic) rolls back automatically if the upgrade does not become healthy in time.
# Look before you install
helm show values oci://registry.example.com/charts/my-app --version 1.4.2 > defaults.yaml
# Install or upgrade, pinned, waiting for readiness
helm upgrade --install my-app oci://registry.example.com/charts/my-app \
--version 1.4.2 \
--namespace my-app --create-namespace \
-f values-production.yaml \
--wait --timeout 5m --rollback-on-failure
helm list -n my-app
helm history my-app -n my-app
helm rollback my-app 3 -n my-app
03Writing your own chart
helm create my-app generates a working chart for a typical web service, which you then trim. Templates read values with {{ .Values.image.tag }}, built-in objects such as {{ .Release.Name }} and {{ .Chart.AppVersion }}, and functions like default, quote and toYaml. nindent places blocks of YAML at the right indentation, which is where most template bugs hide. Shared snippets, such as standard labels, go into _helpers.tpl as named templates and are included with include. Keep values.yaml well commented, because it is the chart's public interface. Chart.yaml has two versions: version is the chart's own semantic version, bumped on every chart change; appVersion is the application version it deploys, informational by default.
# templates/deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
resources: {{- toYaml .Values.resources | nindent 12 }}
04Values per environment
values.yaml in the chart holds safe defaults. Each environment gets a small override file containing only what differs: production might raise replicas, set resource limits, enable autoscaling and use a real hostname. Pass several files with -f; later files win, and --set wins over files, but keep --set for one-off values such as the image tag from CI, so the configuration stays reviewable in Git. Never put secrets in values files committed to Git. Reference existing Kubernetes Secrets by name, or create them with tools such as External Secrets Operator or Sealed Secrets, or decrypt at deploy time with the helm-secrets plugin and SOPS. A values.schema.json file in the chart validates values against a JSON Schema at install time, catching typos like replicaCont before they reach the cluster.
# values-production.yaml - only overrides
replicaCount: 3
image:
repository: registry.example.com/my-app
tag: "" # set by CI: --set image.tag=$GIT_SHA
ingress:
enabled: true
hosts:
- host: app.example.com
paths: [{ path: /, pathType: Prefix }]
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 512Mi }
existingSecret: my-app-env # created by External Secrets, not Helm
05Testing, packaging and publishing
Treat a chart like code with a release process. helm lint checks structure and common mistakes. helm template renders manifests locally without a cluster, so you can review exactly what will be applied or validate it with kubeconform against Kubernetes schemas. helm upgrade --dry-run=server renders and validates against the real API server. The helm diff plugin shows what an upgrade would change, which is invaluable in pull requests. Chart tests are pods in templates/tests annotated as hooks and run with helm test after install. To publish, helm package creates a versioned .tgz, and helm push uploads it to an OCI registry. Sign charts and verify them on install for supply-chain integrity, and run chart-testing (ct) in CI to lint and install changed charts in a temporary cluster such as kind.
helm lint ./charts/my-app
helm template my-app ./charts/my-app -f values-production.yaml | kubeconform -strict -summary
helm diff upgrade my-app ./charts/my-app -n my-app -f values-production.yaml
helm package ./charts/my-app # -> my-app-1.4.2.tgz
helm registry login registry.example.com
helm push my-app-1.4.2.tgz oci://registry.example.com/charts
Use it when
- Installing third-party software on Kubernetes that ships an official chart.
- Deploying your own services to several environments or clusters from one parameterised package.
- Distributing an application to customers or other teams who run it on their own clusters.
- You want release history and simple rollbacks on top of plain Kubernetes.
Skip it when
- You are not on Kubernetes; Docker Compose or a PaaS needs no Helm.
- A handful of simple manifests with small per-environment differences, where Kustomize overlays are clearer.
- Complex day-two operations such as database failover and backups, where an operator manages lifecycle better than templates.
Alternatives
- Kustomize
Template-free overlays and patches on plain YAML, built into kubectl.
- Kubernetes operators
Controllers that manage complex stateful apps continuously, beyond install and upgrade.
- Argo CD / Flux
GitOps controllers that often render Helm charts and keep the cluster in sync with Git.
- Timoni / CUE, jsonnet
Typed or programmable configuration languages for teams that outgrow text templates.
Best practices
- Pin chart versions and image tags; never deploy floating latest versions to production.
- Use helm upgrade --install --wait with --rollback-on-failure (or a GitOps controller) in pipelines.
- Keep environment overrides small and in Git; use --set only for CI-provided values like the image tag.
- Keep secrets out of values files: reference Secrets created by External Secrets, Sealed Secrets or SOPS.
- Add values.schema.json, helm lint, kubeconform and helm diff to CI for every chart change.
- Bump the chart version on every change and publish to an OCI registry.
- Set resource requests, probes and security contexts as defaults in your own charts.
Common mistakes
- Copying a third-party values.yaml wholesale into your repository, then missing upstream default changes on upgrade.
- Indentation errors in templates from forgetting nindent or trimming whitespace with {{- incorrectly.
- Editing resources with kubectl after installing via Helm, causing drift that the next upgrade overwrites.
- Storing passwords in plain values files in Git.
- Putting heavy logic in templates until the chart becomes an unreadable programming language.
Deep dive
06Hooks, ordering and their failure modes
Hooks are resources annotated with helm.sh/hook so Helm runs them at specific lifecycle points: pre-install, pre-upgrade, post-upgrade, pre-delete, test and others. The classic use is a database migration Job in pre-upgrade, so migrations run before new pods start. helm.sh/hook-weight orders hooks, and helm.sh/hook-delete-policy controls cleanup, for example before-hook-creation,hook-succeeded. Pitfalls: hook resources are not managed as part of the release, so they are not removed by uninstall unless a delete policy says so; a failing pre-upgrade hook blocks the upgrade, which is good, but a slow one can hit the timeout; and hooks do not play perfectly with GitOps tools, which translate them into their own sync phases. Keep migrations idempotent and backward compatible, and give hook Jobs a backoffLimit and activeDeadlineSeconds.
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 1
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "up"]
07Server-side apply, field ownership and drift
With client-side apply, Helm computed a three-way merge between the previous release manifest, the new manifest and the live object. Changes made by other actors, such as a HorizontalPodAutoscaler changing replicas or someone running kubectl edit, could be silently overwritten or, worse, missed. Server-side apply, the default for new releases in Helm 4, sends the desired manifest and lets the API server merge it, tracking which manager owns each field in managedFields. If another manager owns a field Helm wants to set, the API reports a conflict instead of silently fighting over it; --force-conflicts takes ownership deliberately. Practical consequences: do not set replicas in the chart when an autoscaler owns them, expect explicit conflict errors where silent overwrites used to happen, and check a release's apply method with helm get metadata before migrating old releases to server-side apply.
08Library charts, dependencies and the umbrella pattern
Dependencies are declared in Chart.yaml with name, version range, repository and an optional condition such as postgresql.enabled; helm dependency update downloads them into charts/ and pins them in Chart.lock. An umbrella chart composes several subcharts into one release, convenient for a platform bundle but risky for unrelated services, because they then share one release lifecycle and one failure domain. A library chart (type: library) contains only named templates, no resources, and lets many service charts share the same Deployment and Service definitions while each keeps its own values. That gives organization-wide standards for labels, probes and security contexts without copying templates. Parent charts can override subchart values under the subchart's name, and global values are visible to every subchart.
# Chart.yaml
apiVersion: v2
name: my-app
version: 1.4.2
appVersion: "2.8.0"
dependencies:
- name: common-templates
version: ~0.6.0
repository: oci://registry.example.com/charts
- name: redis
version: ">=20.0.0 <21.0.0"
repository: oci://registry.example.com/charts
condition: redis.enabled
Real-world scenarios
An upgrade left production half-deployed
Problem
A pipeline ran helm upgrade without waiting. New pods crashed on a missing environment variable, the release was marked deployed, and traffic partly hit failing pods.
How to solve it
Roll back immediately with helm history to find the last good revision and helm rollback my-app <rev> --wait. Fix the pipeline: use helm upgrade --install --wait --timeout 5m --rollback-on-failure so an unhealthy upgrade reverts automatically and the job fails visibly. Make sure the Deployment has readiness probes, otherwise Kubernetes considers crashing-but-starting pods ready. Add values.schema.json with the variable as required, helm lint and a helm diff step in the pull request. Consider a staging deploy with the same values structure before production.
Upgrading a third-party chart across a major version
Problem
An ingress controller chart is two major versions behind. The team copied its full values.yaml years ago and is afraid of the upgrade.
How to solve it
Reduce the copied file to only real overrides by diffing it against helm show values for the current version. Read the chart's changelog and upgrade notes for both major versions, noting renamed values and CRD changes. Render both versions with helm template and compare, or use helm diff upgrade against the live cluster. Upgrade staging first, one major version at a time, checking CRDs, which Helm does not upgrade automatically from the crds/ folder. Watch metrics and error rates, then repeat in production during a quiet window with a rollback plan.
Ten microservices with ten copied charts
Problem
Each service has its own chart created by helm create years ago. Labels, probes and security settings differ, and a security fix must be repeated ten times.
How to solve it
Create a library chart with named templates for Deployment, Service, Ingress, HPA and a default secure securityContext, published to the OCI registry with semantic versions. Convert one service chart to depend on it, keeping only values and small service-specific templates, and verify with helm template that output is equivalent. Migrate the other services gradually. Add values.schema.json to standardise the interface. Now a fix is a library release plus version bumps, which Renovate can propose automatically across all service repositories.
Cheat sheet
- Install or upgrade safely
helm upgrade --install my-app ./chart -n my-app --create-namespace --wait --rollback-on-failure- Show chart defaults
helm show values oci://registry.example.com/charts/my-app --version 1.4.2- Render locally
helm template my-app ./chart -f values-production.yaml- Validate against the cluster
helm upgrade --install my-app ./chart --dry-run=server- Values of a live release
helm get values my-app -n my-app --all- Release history
helm history my-app -n my-app- Roll back
helm rollback my-app 3 -n my-app --wait- Lock dependencies
helm dependency update ./chart- Package and push
helm package ./chart && helm push my-app-1.4.2.tgz oci://registry.example.com/charts
Questions & answers
Q1What is the difference between a chart, a release and a revision?
A chart is the package: templates, default values and metadata, versioned with a semantic version. A release is one installed instance of that chart in a cluster, identified by name and namespace; you could install the same chart twice as two releases. A revision is a numbered snapshot of a release: every install, upgrade or rollback creates a new revision that records the rendered manifests and values used. Helm stores revisions as Secrets in the release namespace, which is how helm history and helm rollback work. Rolling back to revision 3 actually creates a new revision whose content equals revision 3, so history is never rewritten.
Q2Helm or Kustomize?
Helm is a templating and packaging tool: charts with parameters, versioned distribution, dependencies and release history with rollback. It shines for third-party software and for apps deployed with many variations. Kustomize works on plain YAML with overlays and patches, no templating language, built into kubectl; it is great when you own the manifests and environments differ only slightly. They are not exclusive. Many teams install vendor software with Helm and manage their own services with Kustomize, and Kustomize can even inflate Helm charts. My rule of thumb: if you need to distribute or heavily parameterise, Helm; if you want to read exactly the YAML that is applied, Kustomize.
Q3How does helm rollback work and what does it not undo?
helm rollback my-app 3 takes the manifests stored in revision 3 and applies them again as a new revision, so Deployments, ConfigMaps and other resources return to that state. It does not undo things outside Kubernetes objects: database migrations that ran, data written by the new version, or external resources such as DNS or cloud buckets. It also cannot restore resources that were deleted manually outside Helm without issues, and PersistentVolumeClaims usually keep their data regardless. That is why database changes should be backward compatible and migrations run as separate, deliberate steps. Rollback is reliable for stateless configuration and images, not for state.
Q4How do you manage secrets with Helm?
The rule is that secret values do not live in plain values files in Git. Options in order of my preference: have the chart reference an existing Secret by name and create that Secret with External Secrets Operator from a cloud secret manager or Vault; use Sealed Secrets, which encrypts secrets with a cluster key so the encrypted form can live in Git; or use SOPS with the helm-secrets plugin to decrypt encrypted values files at deploy time. Also remember that Helm stores rendered manifests in release Secrets, so anyone who can read Secrets in the namespace can read values passed to Helm. RBAC on the namespace therefore matters as much as how values are passed.
Q5What changed with Helm 4?
Helm 4, released in late 2025, is the first major version since Helm 3 in 2019. The biggest change is server-side apply for new installs: Kubernetes itself merges changes and tracks field ownership, replacing Helm 3's client-side three-way merge; existing releases keep their previous method unless you opt in. Several flags were renamed, for example --atomic became --rollback-on-failure and --force became --force-replace. There is a new plugin system with WebAssembly-based plugins, better readiness checking with kstatus, and improved OCI support including installing by digest. Charts using the v2 API keep working, and release storage is compatible, so existing Helm 3 releases can be managed by Helm 4 without migration. Helm 3 gets security fixes only for a limited time.
Senior-level questions
S1How do Helm and GitOps tools like Argo CD fit together?
Argo CD and Flux can render Helm charts and apply the result, continuously reconciling the cluster with Git. In that model Helm is mainly a templating and packaging format: Argo CD renders with helm template and applies with its own engine, so helm list shows nothing and Argo's history replaces Helm's revisions. Flux's helm-controller, by contrast, performs real Helm releases. Benefits of GitOps: drift detection, no cluster credentials in CI, and Git as the audit log. Costs: hooks map to sync phases imperfectly, and debugging spans two tools. I would keep charts generic and versioned in a registry, keep environment values in a GitOps repository, and let CI only bump image tags or chart versions there through pull requests.
S2When do Helm templates become the wrong abstraction?
Text templating YAML works until charts accumulate deep conditionals, loops over nested maps and dozens of toggles. Then nobody can predict the output, values.yaml becomes an undocumented API with hundreds of keys, and small changes break indentation in obscure branches. Signs it is time to change: you write logic rather than parameters, you test charts mainly by rendering and eyeballing, or one chart tries to serve fundamentally different deployment shapes. Options: split into smaller charts with a shared library chart; move variation into Kustomize overlays on rendered output; use a typed configuration language such as CUE or a tool like Timoni; or, for complex lifecycle logic, write an operator. The trade-off is ecosystem reach: Helm is universally understood, while alternatives demand new skills.
S3How do you design a chart that other teams or customers will install?
Treat values.yaml as a public API with semantic versioning: renaming a key is a breaking change and needs a major version. Provide secure, working defaults: non-root securityContext, resource requests, probes, and no hard-coded hostnames. Validate with values.schema.json and document every value. Support existingSecret patterns instead of requiring secrets in values. Make optional components conditional, and avoid cluster-scoped resources unless clearly needed, since many installers lack cluster-admin. Handle CRDs carefully, because Helm installs but does not upgrade those in crds/; many projects ship them as a separate chart. Sign and publish to an OCI registry, keep a changelog with upgrade notes, and test install and upgrade paths in CI with chart-testing on kind.
Glossary
- Chart
- A Helm package: templates, default values and metadata for a Kubernetes application.
- Release
- One installed instance of a chart in a cluster, with a name and namespace.
- Revision
- A numbered snapshot of a release created by each install, upgrade or rollback.
- Values
- Configuration inputs that fill a chart's templates, from files or --set.
- Hook
- A resource Helm runs at a lifecycle point, such as a migration Job before upgrade.
- Library chart
- A chart containing only reusable named templates, shared by other charts.
- OCI registry
- A container registry that also stores Helm charts as versioned artifacts.
- Server-side apply
- Kubernetes merges desired state on the API server and tracks which tool owns each field.
- values.schema.json
- A JSON Schema in the chart that validates provided values at install time.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.