Helm Charts Demystified: What They Actually Do and When to Skip Them
A package manager for Kubernetes, minus the cargo-culting

Contents
Everybody installs things on Kubernetes the same way. You find the project, you scroll to the README, and there it is: helm repo add, helm install, done. Three commands and a workload appears, fully wired with services, config maps, secrets and a horizontal pod autoscaler you didn’t ask for. Helm has become the default, and like most defaults it’s reached for without much thought about what it actually is or whether you need it.
So let’s pull it apart. Helm is a templating engine with a release ledger bolted on. That’s the whole trick. Understanding those two halves tells you exactly when it earns its keep and when it’s a layer of indirection you’ll come to resent.
What a chart actually is
A chart is a directory of YAML templates plus a values.yaml file of defaults. When you run helm install, Helm reads your values, renders the templates into plain Kubernetes manifests, and applies them. There is no magic in the cluster — the API server only ever sees ordinary Deployments and Services. The magic is purely client-side string substitution.
This is the single most clarifying thing to internalise: Kubernetes has never heard of Helm. There is no Helm operator watching your cluster, no controller reconciling charts. Helm is a command-line tool that turns templates into YAML and pipes that YAML at the same API your kubectl apply uses. Everything else — the values, the hooks, the release tracking — is bookkeeping that happens around that one act of rendering. Once you stop imagining a server-side component that doesn’t exist, the whole tool gets less mysterious and a lot easier to debug.
A trimmed template looks like this:
| |
You override the defaults at install time, either with flags or your own values file:
| |
That {{ .Values.replicaCount }} is the entire value proposition. One chart, many environments, no copy-pasted manifests drifting out of sync. The directory itself has a fixed shape — a Chart.yaml with metadata and a version, the values.yaml of defaults, a templates/ folder, and an optional charts/ for bundled dependencies:
| |
The _helpers.tpl file is where the recurring boilerplate lives — label blocks, name-truncation logic, the fully-qualified app name. It is also, not coincidentally, where most of the unreadable Helm you will ever meet lives. More on that later.
The release ledger is the underrated bit
The templating gets the attention, but the release tracking is where Helm quietly earns its place. Every install creates a release — a named, versioned snapshot of what got applied — stored as a secret in the cluster. This gives you three genuinely useful things.
| |
That last one matters more than it sounds. kubectl apply -f is happy to create resources but has no idea what belonged to what, so cleaning up is a manual scavenger hunt. Helm knows precisely which objects it owns and can remove them cleanly. The rollback is honest, too — it re-applies the previous manifests, so if your bad upgrade was a config change, you’re back in seconds.
Under the bonnet, the release is just a Secret in the namespace, base64-and-gzip-encoded. You can see them with kubectl get secret -l owner=helm, and that fact occasionally matters: a few thousand revisions of a busy chart will quietly fill etcd with old release secrets, which is why --history-max (default 10) exists. I have watched a CI pipeline that ran helm upgrade on every commit balloon a namespace with hundreds of dead release objects before anyone noticed.
One more piece of currency worth knowing: Helm 4 shipped in late 2025, the first major version in six years, and Helm 3 moved into bug-fix-only support. The mental model in this post is unchanged across both — templating plus a release ledger — but if you are reading older tutorials, check whether they assume Helm 2’s server-side Tiller component. Tiller is long dead; if a guide tells you to helm init and grant a service account cluster-admin, close the tab.
When Helm is the right call
Helm shines when you’re consuming other people’s software. Installing ingress-nginx, cert-manager, Prometheus or a database operator via its official chart means someone has already done the tedious work of parameterising fifty resources sensibly. You set a handful of values and get a battle-tested deployment. Reinventing that by hand would be slow and worse.
It also shines when you genuinely run the same application across dev, staging and production with only values differing. One chart, three values files, and your environments stay structurally identical. That’s a real maintenance win. It’s the same instinct that makes a static-site generator’s data-driven templating worthwhile: one source of truth, many rendered outputs, no drift between copies you maintain by hand.
When to skip it
If you’re deploying a single app you wrote, with a handful of manifests that rarely change, Helm is overhead. You’ve taken readable YAML and buried it under Go templating syntax that turns a missing space into a baffling render error. Debugging nindent alignment at 11pm is nobody’s idea of fun.
For that case, plain manifests with Kustomize are often the better fit. Kustomize patches a base with overlays using ordinary YAML — no templating language, no release secrets, and kubectl has it built in via kubectl apply -k. A base directory holds the canonical manifests, and each environment is an overlay that patches only what differs:
| |
You lose the rollback ledger, but you gain manifests you can read without rendering them first. (One caveat worth tracking: there is a long-running proposal to eventually remove the Kustomize built into kubectl, so for anything serious, install the standalone kustomize binary rather than relying on the bundled one.)
The two are not mutually exclusive, either. A common grown-up pattern is to let Helm own the release — install and lifecycle — while a post-render hook runs your manifests through Kustomize for the last-mile tweaks the chart’s authors never anticipated. helm upgrade --post-renderer ./kustomize-wrapper.sh gives you the ledger and the readable patch in one pipeline.
A useful habit, even when you do use Helm, is to render before you apply so you can see exactly what’s about to hit the cluster:
| |
No surprises, no trusting the template blindly.
Troubleshooting: the four ways Helm ruins your evening
Charts fail in a small number of recognisable ways, and once you have seen each one a couple of times you stop panicking.
1. nindent and whitespace render errors. Go templates are whitespace-sensitive, and YAML is whitespace-structured, so the two together are a minefield. The classic symptom is error converting YAML to JSON: did not find expected key, which tells you nothing useful. The fix is to render and read, never apply blind:
| |
When a block is indented wrong, {{- toYaml .Values.resources | nindent 12 }} is almost always the culprit — the number after nindent must match the column the block sits at, and the leading {{- trims the preceding newline. Off by two spaces and the whole document is invalid.
2. The upgrade that half-applies. helm upgrade can leave a release stuck in pending-upgrade if it is interrupted (a killed CI job, a flaky API server). Subsequent upgrades then refuse with another operation (install/upgrade/rollback) is in progress. Roll back to the last good revision to clear the lock:
| |
3. Values that silently do nothing. A typo in --set imagе.tag (or a key your chart never reads) is not an error — Helm happily accepts unknown values and ignores them. If a change “isn’t taking”, diff the rendered output against what you expect rather than assuming the value reached a template. The helm get values myapp and helm get manifest myapp commands show what the cluster actually believes.
4. CRDs and ordering. Helm installs Custom Resource Definitions before the rest of the release, but it does not upgrade or delete them on subsequent runs — by design, to avoid wiping data. If a chart’s new version expects a CRD field that isn’t there, you get cryptic admission errors. Read the chart’s upgrade notes; sometimes you must kubectl apply the new CRDs by hand first.
The honest verdict
Helm is worth learning, full stop — too much of the ecosystem ships as charts to avoid it. But it is a tool, not a religion. Use it to consume third-party software and to manage genuinely multi-environment apps, where its templating and rollback ledger pay for themselves. Reach for Kustomize or plain manifests when your needs are simpler than the abstraction.
The failure mode I see most often is people writing elaborate charts for a single in-house service that gets deployed to exactly one place, then spending more time fighting whitespace in templates than they ever spent on the app. If you find yourself there, you’ve out-engineered the problem. Strip it back. The best infrastructure tool is the smallest one that does the job, and sometimes that’s a folder of honest YAML and a kubectl apply.
Who is this for? If you run other people’s software on Kubernetes — and almost everyone does, the moment they install an ingress controller or a metrics stack — you will use Helm whether you like it or not, so learn the four-command core (install, upgrade, rollback, template) and the troubleshooting above. If you are shipping your own single app to a single cluster, try plain manifests or Kustomize first and only reach for a chart when you feel the pain of copy-pasted environments. And if you are debugging a chart someone else wrote, the same discipline that helps you trace what actually happens when a pod looks up a name applies here: render the truth, read it, stop trusting the abstraction. Helm earns its keep. It just doesn’t earn a free pass.




