Helm Chart Anatomy
Introduction¶
Helm packages Kubernetes manifests as templated, versioned, parameterised charts. A chart is just a directory with a specific layout.
Layout¶
mychart/
├── Chart.yaml # name, version, appVersion, dependencies
├── values.yaml # default configuration
├── values.schema.json # optional: JSON Schema to validate values
├── templates/
│ ├── _helpers.tpl # named template definitions (no output on its own)
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── NOTES.txt # printed after install
│ └── tests/
│ └── test-connection.yaml
├── charts/ # subcharts (dependencies) vendored here
└── .helmignore
Chart.yaml¶
apiVersion: v2
name: mychart
version: 1.4.2 # the CHART version — bump on any chart change
appVersion: "2.7.0" # the app version it deploys (informational; often the default image tag)
dependencies:
- name: postgresql
version: "15.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
helm dependency update pulls dependencies into charts/.
Templates¶
Go's text/template with Sprig functions. Built-in objects:
.Values— merged values (defaults +-ffiles +--set).Release—.Name,.Namespace,.IsInstall,.IsUpgrade,.Revision.Chart— contents of Chart.yaml.Capabilities— cluster version, available API versions.Files— access non-template files in the chart
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- with .Values.resources }}
resources: {{- toYaml . | nindent 12 }}
{{- end }}
Patterns to know:
{{-/-}}trim whitespace (left / right).nindentindents and adds a leading newline.include "name" .calls a named template and returns a string (pipeable);templatedoesn't return, so preferinclude.toYaml . | nindent Nrenders a values sub-tree as YAML.{{- with .Values.x }}sets.toxinside the block, and skips it if empty.default,required "message" .Values.x,quote,b64enc,tpl(render a string as a template).
_helpers.tpl¶
{{- define "mychart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
Hooks¶
Annotations that run a resource at a lifecycle point:
metadata:
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": hook-succeeded
Common use: a Job that runs DB migrations before the new pods roll.
Lifecycle¶
helm install myrel ./mychart -f prod-values.yaml
helm upgrade myrel ./mychart -f prod-values.yaml --atomic --timeout 5m
helm upgrade --install myrel ./mychart ... # idempotent
helm rollback myrel 3
helm uninstall myrel
helm list -A
helm history myrel
Verification and troubleshooting¶
helm template myrel ./mychart -f prod-values.yaml # render locally, no cluster
helm lint ./mychart
helm install myrel ./mychart --dry-run --debug
helm get manifest myrel # what's actually deployed
helm get values myrel # the values used
helm diff upgrade myrel ./mychart -f prod-values.yaml # (helm-diff plugin)
nindentoutput misaligned / "error converting YAML" — wrong indent count, or usedindent(no newline) wherenindentwas needed, or a{{-ate a newline you needed.helm templateand look at the raw output.--setnested value not applied — quoting.--set image.tag=v2works; values with dots/commas need--set 'a.b=x\,y'or a-ffile.- Upgrade leaves orphaned resources — Helm only tracks what's in the current
manifest; a template you removed is deleted on upgrade, but resources created
by hooks or outside Helm aren't. Use
--atomicso a failed upgrade rolls back. another operation (install/upgrade/rollback) is in progress— a previous command died.helm rollbackto the last good revision, or (last resort) patch out the pending-* status secret.- Subchart values not taking — they nest under the subchart name:
postgresql: { auth: { password: ... } }in the parentvalues.yaml. checksum/configdidn't trigger a rollout — the referenced template path is wrong; it must resolve to the actual ConfigMap template file.
Related tools and reading¶
- On-site: YAML validator, JSON formatter.
- Related posts: kubectl commands you use daily, ConfigMaps and Secrets done right.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Docker & Kubernetes
StatefulSets Explained
StatefulSets give pods sticky names, sticky storage, and ordered operations — what databases and clustered apps need. Here's how each guarantee works.
October 24, 2026Persistent Volumes and Claims, Explained
PVC is a request, PV is the fulfilled storage, StorageClass is the factory. Most 'Pending PVC' problems are an access-mode or provisioner mismatch.
October 24, 2026Kubernetes Jobs and CronJobs
A Job runs pods until they succeed; a CronJob creates Jobs on a schedule. Here's parallelism, retry limits, the missed-schedule trap, and history cleanup.
October 24, 2026