Helpers & Named Templates
Eliminate duplication and enforce consistency across your Helm charts using _helpers.tpl, define/include, and reusable template snippets.
Introduction
As Helm charts grow beyond a single Deployment and Service, you quickly notice the same YAML fragments appearing in multiple template files: resource names, label blocks, annotation sets, image pull secret lists. Copy-pasting these fragments is fragile — a label change in one file but not another silently breaks selector matching, and a typo in a resource name leads to hours of debugging.
Helm solves this with named templates (also called "partials" or "helpers"). You define a reusable snippet once, give it a name, and invoke it wherever needed. The convention file for these snippets is templates/_helpers.tpl, though any file prefixed with an underscore works. This lesson covers the mechanics of defining, invoking, and composing named templates, then walks through every helper that helm create generates by default.
The _helpers.tpl Convention
Helm's template engine processes every file inside the templates/ directory. Files whose names begin with an underscore (_) are special: they are parsed for template definitions but never rendered as standalone Kubernetes manifests. This makes them the perfect home for reusable snippets.
By convention, charts place all named templates in a single file called _helpers.tpl. You are free to split helpers across multiple underscore-prefixed files (e.g., _labels.tpl, _probes.tpl), but the single-file convention is overwhelmingly standard and what helm create produces.
_helpers.tpl contains only define blocks and no actual resource YAML, the API server would reject it. The underscore tells Helm: "parse this file for definitions, but do not output it."
Defining Named Templates
A named template is created with the define action and closed with end:
{{- 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 }}
Key points about the syntax:
- The name string (
"mychart.fullname") is globally unique across the chart and all its subcharts. Always prefix with the chart name to avoid collisions. - The
{{- ... -}}dashes trim whitespace on the left (-after opening braces) and right (-before closing braces). Without them, you get blank lines and broken indentation in the rendered YAML. - The body of a
defineblock has access to whatever scope (.) is passed when the template is invoked.
include vs template
Go's standard library provides the template action to invoke a named template. Helm adds a custom include function. The difference is critical:
| Feature | template | include |
|---|---|---|
| Origin | Go standard library | Helm-specific function |
| Return value | Writes directly to output (returns nothing) | Returns a string |
| Pipeline support | No — cannot pipe result | Yes — pipe into nindent, toYaml, etc. |
| Recommended | Rarely used in Helm charts | Always preferred |
Because template writes directly to the output stream, you cannot post-process its result. This means the following does not work:
{{/* BROKEN - template cannot be piped */}}
labels:
{{ template "mychart.labels" . | nindent 4 }}
The pipeline (| nindent 4) applies to the return value, but template returns nothing — the labels have already been written to output without indentation. The correct approach:
{{/* CORRECT - include returns a string */}}
labels:
{{- include "mychart.labels" . | nindent 4 }}
include in Helm charts. Reserve template for the rare case where you are writing raw Go templates outside of Helm.
The Standard Helpers from helm create
When you run helm create mychart, Helm scaffolds a _helpers.tpl with six named templates. Understanding these is essential because nearly every production chart follows this pattern.
mychart.name
Returns the chart name (from Chart.yaml), allowing an override via .Values.nameOverride. Truncated to 63 characters to comply with Kubernetes DNS label limits.
{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
mychart.fullname
Produces a unique resource name by combining the release name and the chart name. Supports fullnameOverride for complete control, and nameOverride for the chart-name portion only. Also truncated to 63 characters.
mychart.chart
Returns a name-version string (e.g., mychart-0.1.0) used for the helm.sh/chart label. Version dots and plus signs are replaced to keep the label value DNS-safe.
{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
mychart.labels
Produces the full set of recommended Kubernetes labels. It calls mychart.selectorLabels internally and adds informational labels:
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
mychart.selectorLabels
The minimal label set used in Deployment selectors and Service selectors. These labels must remain immutable after the first install — Kubernetes forbids changing a Deployment's spec.selector.
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
mychart.serviceAccountName
Conditionally returns either a user-specified name or the chart's fullname, depending on whether ServiceAccount creation is enabled:
{{- define "mychart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mychart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
Writing Custom Helpers
The standard helpers cover naming and labeling, but real charts need more. Here are two commonly needed custom helpers.
Image Pull Secret Helper
Many organizations use private container registries. Rather than repeating the imagePullSecrets block in every Deployment and Job, extract it into a helper:
{{- define "mychart.imagePullSecrets" -}}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- range . }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end }}
Usage: {{- include "mychart.imagePullSecrets" . | nindent 6 }}
Annotation Merge Helper
When you need to combine chart-level annotations with resource-specific annotations:
{{- define "mychart.mergeAnnotations" -}}
{{- $common := .common | default dict -}}
{{- $extra := .extra | default dict -}}
{{- toYaml (merge $extra $common) -}}
{{- end }}
Invoke it by passing a custom dict:
annotations:
{{- include "mychart.mergeAnnotations" (dict "common" .Values.commonAnnotations "extra" .Values.serviceAnnotations) | nindent 4 }}
Scope and the Dot
Every Go template executes within a scope — the value of . (dot). When Helm renders a template file, dot starts as the root context containing .Values, .Release, .Chart, and .Capabilities. When you call include, the second argument becomes the new dot for the called template.
This matters inside control structures that rebind dot:
{{- range .Values.containers }}
{{/* . is now the current container, not root */}}
name: {{ .name }}
image: {{ .image }}
labels:
{{- include "mychart.labels" $ | nindent 8 }}
{{/* $ always refers to the root scope */}}
{{- end }}
The $ variable is set once at the start of template execution and never changes, making it your escape hatch when dot has been rebound by range, with, or other actions.
$ inside a helper, consider whether the helper should accept a more specific scope (like a dict with only the values it needs) rather than the entire root context.
DRY Patterns: Extracting Repeated YAML
Beyond labels and names, look for any YAML block that appears in more than one template file. Common candidates include:
- Health probes — liveness, readiness, and startup probes with the same HTTP path and port pattern
- Resource limits — CPU/memory requests and limits that follow a standard profile
- Security contexts — pod and container security settings that must be consistent
- Affinity rules — anti-affinity rules that should apply to all workloads
- Tolerations — node tolerations shared across Deployments and Jobs
Extract each into a define block, parameterize the variable parts through scope, and invoke with include plus nindent.
Overriding Helpers in Parent Charts
Helm processes templates in a specific order: subcharts are loaded before the parent chart. When two define blocks share the same name, the last definition wins. This means a parent chart can override any helper defined by a subchart.
For example, if subchart backend defines "backend.fullname", the parent chart can redefine it:
{{/* Parent chart's _helpers.tpl */}}
{{- define "backend.fullname" -}}
{{- printf "%s-api" .Release.Name -}}
{{- end }}
This is powerful but requires care. Always document which helpers are intended as extension points. The naming convention (chartname.helpername) makes it explicit which chart originally owns each helper.
Partials for Complex Resources
For charts that generate many similar resources (e.g., multiple microservices in a mono-chart), you can use helpers as partials that produce entire resource sections. Combined with range over a list in values, this pattern can generate multiple Deployments from a single template file:
{{- define "mychart.deploymentSpec" -}}
spec:
replicas: {{ .replicas | default 1 }}
selector:
matchLabels:
app: {{ .name }}
template:
metadata:
labels:
app: {{ .name }}
spec:
containers:
- name: {{ .name }}
image: "{{ .image }}:{{ .tag | default "latest" }}"
ports:
- containerPort: {{ .port | default 8080 }}
{{- end }}
This technique is an advanced pattern. For most charts, keeping helpers focused on small, composable fragments (labels, probes, annotations) produces more maintainable code than generating entire resources from helpers.
Summary
Named templates are the backbone of maintainable Helm charts. The key takeaways:
- Place helpers in
templates/_helpers.tpl— underscore-prefixed files are parsed but not rendered. - Use
defineto create andincludeto invoke — nevertemplate, which cannot be piped. - The six standard helpers from
helm create(name, fullname, chart, labels, selectorLabels, serviceAccountName) form the foundation of every chart. - Prefix helper names with the chart name to avoid collisions in subchart scenarios.
- Pass the right scope: use
.for full context,$to escape rebound scope, or custom dicts for targeted data. - Parent charts can override subchart helpers — the last definition wins.