How Helm's Go-template rendering, values overrides, chart dependencies, and release lifecycle actually work under the hood, and the debugging habits — helm template, dry-run, hooks — that save you from a bad upgrade. Most Helm pain I've seen isn't Helm being broken, it's someone treating a chart as a black box instead of just rendering it and reading the output.
Templating is Go templates plus Sprig, nothing more exotic
A Helm chart's templates are plain Go `text/template` syntax with the Sprig function library layered on top for the things Go templates don't do natively — string manipulation, list operations, default values, date formatting. `{{ .Values.replicaCount }}` pulls from values.yaml, `{{ .Release.Name }}` and `{{ .Release.Namespace }}` come from the release context Helm injects, and helpers defined in `_helpers.tpl` get called with `{{ include "mychart.fullname" . }}`. There's no magic beyond that — if a template isn't rendering the way you expect, it's a Go template problem, and it's easier to debug by rendering it locally than by staring at the chart source trying to trace values by eye.
replicaCount: 2
image:
repository: registry.example.com/checkout-api
tag: "1.4.2"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
ingress:
enabled: true
hostname: checkout.example.com
postgresql:
enabled: true
auth:
database: checkout
Overrides layer on top of the chart's default values.yaml in a defined order: `--set key=value` on the command line wins over `-f custom-values.yaml`, which wins over the chart's own values.yaml. `--set` is fine for a one-off like a tag bump; anything meant to persist — an environment's actual config — belongs in a values file that's committed and passed with `-f`, because `--set` history lives only in your shell and in `helm get values`, not in a diffable file.
Dependencies pull in subcharts, not magic
A chart declares dependencies in `Chart.yaml` under a `dependencies` block — name, version, and repository. Running `helm dependency update` resolves those against a `Chart.lock` and downloads the actual chart archives into a `charts/` subdirectory, which is what gets packaged and installed alongside your own templates. This is how a chart for your app ends up quietly installing PostgreSQL or Redis as a subchart: the parent chart's values.yaml has a `postgresql:` block that maps directly onto the dependency chart's own values, scoped under that key. It's ordinary chart composition, not a separate mechanism — worth checking `charts/` after a dependency update to confirm you got the version you expected, since a loose version range in Chart.yaml can silently pull a newer subchart than you tested against.
apiVersion: v2
name: checkout-api
version: 0.4.0
appVersion: "1.4.2"
dependencies:
- name: postgresql
version: "15.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
helm dependency update re-resolves against the repositories and rewrites Chart.lock — use it when you've changed the dependencies block. helm dependency build just re-downloads charts.tgz files to match an existing Chart.lock — use it in CI, where you want reproducible installs, not fresh resolution.
Release state lives in a Secret, in-cluster
Every `helm install` or `helm upgrade` writes a new release record as a Kubernetes Secret (by default) in the release's namespace, labeled with the release name and revision number. That's what makes `helm rollback` possible — it's not reapplying old YAML from your local disk, it's reading the previous revision's stored manifest straight out of that Secret and applying it. It also means `helm history
If an upgrade fails partway, the release can land in a failed state that blocks the next upgrade until you run helm rollback manually. --atomic makes Helm roll back automatically on a failed upgrade, restoring the prior working release without a manual step at 2am.
helm template vs. install --dry-run for debugging
helm template renders the chart to plain YAML entirely client-side — no cluster contact at all, which makes it the fastest way to check "does this render the way I expect" and to diff output between two values files. helm install --dry-run (or upgrade --dry-run) does the same rendering but also talks to the cluster to run server-side validation against the actual API server, which catches problems `helm template` can't see: an invalid field for your cluster's API version, a webhook admission controller rejecting the manifest, or a CRD that doesn't exist yet. I reach for `helm template` first for a quick look and `--dry-run` right before a real upgrade, when I want the closest thing to a real preflight check.
helm template checkout-api ./chart -f values-prod.yaml
helm upgrade checkout-api ./chart \
-f values-prod.yaml \
--dry-run --debug \
--atomic --timeout 5m
Hooks run outside the normal apply order
Chart hooks — annotated with `helm.sh/hook: pre-install`, `post-upgrade`, `pre-delete`, and so on — are resources Helm applies at specific points in the release lifecycle, outside the normal manifest ordering. A `pre-install` hook commonly runs a Job that creates a database schema before the application Deployment starts; a `post-upgrade` hook might run a smoke test or a cache-warming Job. Hooks aren't tracked as part of the regular release the same way — by default they aren't deleted on `helm uninstall` unless you set a hook-deletion-policy — which trips people up when a one-off migration Job from months ago is still sitting in the namespace.
Wrapping up
Helm's moving parts are simpler than they get credit for: Go templates plus Sprig for rendering, a layered values system for overrides, Chart.yaml dependencies for composing subcharts, and release state stored as a Secret in-cluster that makes rollback possible. The habit that prevents most bad upgrades is cheap — render with `helm template` or dry-run before you apply anything for real, and use `--atomic` so a failed upgrade doesn't leave the release stuck for the next person.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.