Search across all documentation pages
_helpers.tpl, include, named templates, values.schema.json, chart vs app version, hooks.Start from the scaffold, then shape it deliberately.
helm create myappDefine your values surface in values.yaml, factor naming into _helpers.tpl, add a values.schema.json to validate input, pin subcharts in Chart.yaml, and version the chart with semver on every change.
A reusable helper for consistent names and labels lives in templates/_helpers.tpl.
# templates/_helpers.tpl
{{- define "app.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "app.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}The Deployment consumes those helpers with include, keeping templates thin.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "app.fullname" . }}
labels:
{{- include "app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
resources:
{{- toYaml .Values.resources | nindent 12 }}
readinessProbe:
httpGet:
path: /healthz
port: 8080A JSON schema rejects bad input before rendering.
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["replicaCount", "image"],
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" }
}
}
}
}includeUse define in _helpers.tpl to name a reusable block, then call it with include "name" ..
Prefer include over the older template action because include returns a string you can pipe into nindent for correct indentation.
Centralize names and labels in helpers so every object stays consistent and the recommended app.kubernetes.io/* labels appear everywhere.
Declare dependencies in Chart.yaml, then run helm dependency update to vendor them into charts/ and write Chart.lock.
# Chart.yaml
dependencies:
- name: redis
version: "20.1.x"
repository: https://charts.example.com
condition: redis.enabled
alias: cacheParent values flow to a subchart under a key matching its name (or alias), so cache.architecture in the parent sets the aliased Redis subchart.
Use condition to let consumers disable a subchart, and alias when you need two instances of the same chart.
Global values (.Values.global.*) are shared across the parent and all subcharts, which is the correct place for cross-cutting settings like an image registry.
version in Chart.yaml is the chart's own semver and must be bumped on any chart change.
appVersion tracks the deployed application and is informational; use it as the default image tag via .Chart.AppVersion.
Bump version for template or default changes, and bump appVersion when the packaged application releases a new build.
Annotate a resource with helm.sh/hook to run it at a phase, such as a migration Job before upgrade.
# templates/migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "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:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["/app/migrate"]hook-weight orders multiple hooks, and hook-delete-policy controls cleanup so old hook Jobs do not accumulate.
Whitespace bugs are the most common failure; use {{- and -}} to trim, and always pipe include output through nindent at the correct depth.
Forgetting to bump version breaks GitOps tools that key on chart version to detect drift; automate the bump in CI.
Overusing subcharts creates deep value-override chains that are hard to trace; flatten where a subchart adds no real reuse.
Leaving helm.sh/hook-delete-policy unset leaves stale hook resources behind after every release.
Putting environment-specific defaults in values.yaml couples the chart to one site; keep the chart generic and pass env values from overlay files.
Kustomize suits teams that own their manifests and want overlays instead of a templating language; see the Kustomize page.
Plain manifests plus a thin kubectl apply work for tiny, single-environment services where a chart is overkill.
A library chart (type: library) packages only helpers for other charts to import, with no installable resources of its own.
CUE or jsonnet-based tools offer stronger typing than Go templates, at the cost of a smaller ecosystem and steeper learning curve.
Should the chart version equal the app version? No. The chart version tracks chart changes; appVersion tracks the application. They move independently.
How do I share config with subcharts? Use .Values.global.* for cross-cutting values, or set subchart values under the subchart's name key.
Where do reusable snippets go? In _helpers.tpl as named define blocks, invoked with include.
How do I validate user input? Ship a values.schema.json; Helm validates supplied values against it at install and upgrade.
When should I use a hook? For lifecycle steps like migrations or backups that must run before or after the main install or upgrade.
Can I run two copies of one subchart? Yes, declare it twice with different alias values.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026