Helm Charts Explained
Helm is the package manager for Kubernetes: it bundles manifests into versioned charts and tracks each install as a release. This guide covers templating, dependencies, hooks, testing and automation.
Core concepts
Raw Kubernetes manifests are static: running the same application in dev, staging and production usually means copying YAML and editing it. Helm solves this with three ideas.
- Chart: a package of templated manifests plus metadata and default configuration.
- Release: one installed instance of a chart in a cluster, with its own name and revision history.
- Repository: an HTTP or OCI location that stores packaged charts.
Since Helm 3, Helm is client-only. It renders templates locally, sends the result to the Kubernetes API, and by default stores release state as Secrets in the release namespace, so there is no server component to run.
Chart structure
mychart/
Chart.yaml # name, version, appVersion, dependencies
values.yaml # default configuration
values.schema.json # optional validation of values
charts/ # packaged dependencies
templates/
deployment.yaml
service.yaml
_helpers.tpl # named templates, not rendered directly
NOTES.txt # message printed after install
tests/ # test hooks
Chart.yaml requires apiVersion, name and version. The chart version follows semantic versioning and changes whenever the chart changes; appVersion describes the application it deploys and is informational. Scaffold a new chart with helm create mychart.
Templates and values
Templates use Go templating. Values come from values.yaml, overridden by -f other.yaml files and finally by --set key=value, with later sources winning. Inside a template you read them through built-in objects: .Values, .Release (name, namespace, revision), .Chart and .Capabilities.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
# selector and pod labels omitted for brevity
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.env }}
env:
{{- range $name, $val := .Values.env }}
- name: {{ $name }}
value: {{ $val | quote }}
{{- end }}
{{- end }}
Flow control, pipelines and reuse
if/else,withandrangecontrol what gets rendered; the{{-form trims surrounding whitespace, which matters because YAML is indentation sensitive.- Pipelines chain functions:
quote,default,upper,toYaml,indent/nindent,required. - Named templates defined with
definein_helpers.tplare invoked withinclude, which (unliketemplate) can be piped. Use them for shared labels and names. - A library chart contains only named templates and is reused by other charts.
Use values.schema.json to enforce types and required keys, and required or fail in templates to give clear error messages on bad input.
Dependencies
A chart can depend on other charts, such as a database. Declare them in Chart.yaml:
dependencies:
- name: postgresql
version: "15.x.x"
repository: https://charts.example.com
condition: postgresql.enabled
Run helm dependency update to download them into charts/ and write a lock file; helm dependency build reproduces exactly what the lock file pins. A parent overrides a child's values by nesting them under the child's name in its own values file, so postgresql.auth.database reaches the subchart as auth.database. The condition and tags fields toggle subcharts, and import-values can pull selected child values up into the parent.
Hooks
Hooks run Kubernetes resources, typically Jobs, at specific points in the release lifecycle. You mark a template with an annotation:
metadata:
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": hook-succeeded
Available events include pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, pre-rollback, post-rollback and test. Weights order hooks of the same type, and the delete policy cleans up finished Jobs. Typical uses are database migrations before an upgrade and smoke checks afterwards.
Hook resources are not part of the tracked release manifest, so Helm will not remove them on uninstall unless a delete policy says so. A failing pre-upgrade hook blocks the upgrade, which is intentional but can surprise you.
Release lifecycle
helm repo add example https://charts.example.com
helm repo update
helm search repo wordpress
helm show values example/wordpress > my-values.yaml
helm install blog example/wordpress -n web --create-namespace -f my-values.yaml
helm list -n web
helm get values blog -n web
helm upgrade blog example/wordpress -n web -f my-values.yaml
helm history blog -n web
helm rollback blog 1 -n web
helm uninstall blog -n web
Every install, upgrade and rollback creates a new revision, which is why rollback is cheap and safe. Useful flags: --atomic rolls back automatically if an upgrade fails, --wait blocks until resources are ready, and upgrade --install makes a command idempotent for pipelines. Use --reuse-values carefully; keeping values in version-controlled files is more predictable.
Testing and validation
Check a chart in layers, from cheap to thorough:
helm lint mychartcatches structural and template mistakes.helm template mychart -f values-prod.yamlrenders manifests locally so you can read or diff them.helm install --dry-run=server --debugorhelm upgrade --dry-run=servervalidates against the live API server without creating anything.- Schema validators such as kubeconform can check rendered output against Kubernetes schemas.
helm test <release>runs resources annotated withhelm.sh/hook: test, usually a pod that curls the service and exits non-zero on failure.
Packaging and automation
helm package mychart produces a versioned .tgz. Publish it to a chart repository (a static index.yaml plus archives, generated with helm repo index) or push it to an OCI registry with helm push. Helm can sign packages for provenance.
A typical CI pipeline for a chart repository:
- Lint and render the chart on every pull request, against several values files.
- Install it into a throwaway cluster (for example kind) and run
helm test. - On merge or tag, bump the chart version, package and publish.
- Deploy with
helm upgrade --installplus automatic rollback on failure or hand the chart to a GitOps controller.
Best practices: bump version on every chart change, keep defaults safe and minimal, document every value, avoid hard-coding namespaces, and never put real secrets in values.yaml; reference an existing Secret or use an external secrets tool instead.