Eliminate repetition in your Helm charts with _helpers.tpl, define/include, and reusable template snippets that keep your manifests DRY.
define and includehelm create generatesAny file in templates/ that starts with an underscore is never rendered as a Kubernetes manifest. By convention, reusable template snippets live in _helpers.tpl.
{{/* templates/_helpers.tpl */}}
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
{{- ... -}} dash syntax trims surrounding whitespace, which is critical for clean YAML output.Go provides two ways to invoke named templates. In Helm charts, always prefer include because it allows pipelines.
{{ template "mychart.labels" . }}
{{/* Cannot pipe output! */}}
{{/* This FAILS: */}}
{{ template "mychart.labels" . | nindent 4 }}
template writes directly to output — it returns nothing, so you cannot pipe the result through functions like nindent or toYaml.
{{ include "mychart.labels" . | nindent 4 }}
{{/* Works! include returns a string */}}
{{ include "mychart.annotations" . | indent 8 }}
include captures the output as a string, so you can pipe it into nindent, toYaml, trim, and any other Sprig function.
include is a Helm-specific function, not part of core Go templates. It exists precisely to solve the pipeline limitation of template.helm createRunning helm create mychart generates a _helpers.tpl with six standard helpers. Here is what each one does:
| Helper | Purpose |
|---|---|
mychart.name | Chart name, truncated to 63 chars (DNS label limit) |
mychart.fullname | Release-name + chart-name, respects fullnameOverride |
mychart.chart | name-version string for the helm.sh/chart label |
mychart.labels | Standard set of metadata labels (app, version, managed-by, etc.) |
mychart.selectorLabels | Minimal labels for Deployment/Service selectors (name + instance) |
mychart.serviceAccountName | Conditional: use override or fall back to fullname |
selectorLabels must be immutable after creation (Kubernetes rejects selector changes on Deployments). The full labels helper includes selectorLabels plus informational labels like app.kubernetes.io/version that can change on upgrades.
You can add any helper you need. Two common patterns:
{{- define "mychart.imagePullSecrets" -}}
{{- if .Values.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- define "mychart.annotations" -}}
{{- $base := dict "app.kubernetes.io/managed-by" "Helm" -}}
{{- $extra := .Values.commonAnnotations | default dict -}}
{{- toYaml (merge $extra $base) -}}
{{- end }}
Usage in a template:
metadata:
annotations:
{{- include "mychart.annotations" . | nindent 4 }}
spec:
{{- include "mychart.imagePullSecrets" . | nindent 2 }}
Every template executes with a scope — the value of . (dot). When you call include, the second argument sets the scope for the called template.
{{/* Pass full context */}}
{{ include "mychart.labels" . }}
{{/* Pass only .Values.app */}}
{{ include "mychart.appConfig" .Values.app }}
{{/* Pass a custom dict */}}
{{ include "mychart.resource" (dict "name" "cpu" "limit" "500m") }}
Inside a range loop, . is rebound to the current item. To access the root context, use $:
{{- range .Values.services }}
name: {{ .name }}
chart: {{ include "mychart.chart" $ }}
{{/* $ always points to root scope */}}
{{- end }}
Named templates are the primary DRY mechanism in Helm. Two advanced patterns:
{{- define "mychart.probes" -}}
livenessProbe:
httpGet:
path: {{ .path | default "/healthz" }}
port: {{ .port | default "http" }}
initialDelaySeconds: 10
readinessProbe:
httpGet:
path: {{ .path | default "/readyz" }}
port: {{ .port | default "http" }}
{{- end }}
When chart B depends on chart A, B can redefine any of A's helpers:
{{/* In parent chart B's _helpers.tpl */}}
{{- define "chartA.fullname" -}}
{{- printf "custom-%s" .Release.Name -}}
{{- end }}
The last definition wins. Parent charts load after subcharts, so the parent's version takes precedence.
mychart.labels) to avoid collisions when charts are used as subcharts._helpers.tpl from helm create_helpers.tpl from scratch with 4+ helpersNamed Templates: helm.sh/docs/chart_template_guide/named_templates
Subcharts & Globals: helm.sh/docs/chart_template_guide/subcharts_and_globals