Named Templates and Helpers
Create reusable template snippets with define, template, and include. Keep your Helm charts DRY and maintainable.
Named Templates and Helpers
In the previous tutorial, you probably noticed the same patterns appearing in every template — the same label blocks, the same name generation logic, the same boilerplate. Named templates (also called "partials") solve this by letting you define reusable snippets.
What Goes in _helpers.tpl?
By convention, named templates live in templates/_helpers.tpl. The leading underscore tells Helm "don't render this as a Kubernetes manifest." It's a pure helper file.
# templates/_helpers.tpl
# Chart name (with override support)
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
# Fully qualified app name
{{- define "my-chart.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 }}
define — Creating a Named Template
{{- define "TEMPLATE_NAME" -}}
... template content ...
{{- end }}
The naming convention is chart-name.purpose:
{{- define "my-chart.name" -}}
{{- define "my-chart.fullname" -}}
{{- define "my-chart.labels" -}}
{{- define "my-chart.selectorLabels" -}}
{{- define "my-chart.serviceAccountName" -}}
"Why prefix with the chart name?"
Because named templates are globally scoped across a chart and all its subcharts. If you and a dependency both define "labels", they'll collide. Prefixing with the chart name prevents conflicts.
template vs include — Using Named Templates
There are two ways to call a named template:
The template Action (Don't Use This)
metadata:
labels:
{{ template "my-chart.labels" . }}
The include Function (Use This)
metadata:
labels:
{{- include "my-chart.labels" . | nindent 4 }}
"What's the difference?"
template is a Go built-in action — it directly inserts the output into the template. The problem? You can't pipe its output to functions like nindent or quote.
include is a Helm-specific function that returns the rendered text as a string, so you can pipe it. Since you almost always need to control indentation, always use include.
# template — can't control indentation
metadata:
labels:
{{ template "my-chart.labels" . }}
# Output might be misaligned!
# include — pipe to nindent for correct indentation
metadata:
labels:
{{- include "my-chart.labels" . | nindent 4 }}
# Always correctly indented
The Second Argument — Passing Scope
Both template and include take a second argument: the scope to pass as . inside the template.
# Pass the root context (most common)
{{ include "my-chart.labels" . }}
# Inside "my-chart.labels", the dot (.) is the root scope
# So .Release.Name, .Chart.Name, .Values.x all work
If you forget the second argument, the dot inside the template is nil:
# BROKEN — no scope passed
{{ include "my-chart.labels" }}
# Inside the template, .Release.Name would fail!
You can also pass a custom scope:
# Pass a specific sub-object
{{ include "my-chart.container" .Values.sidecar }}
# Or construct a custom scope with a dict
{{ include "my-chart.container" (dict "values" .Values.sidecar "root" .) }}
The Standard Helpers
When you run helm create my-chart, it generates a _helpers.tpl with several useful templates. Let's understand each one:
Chart Name
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
Returns the chart name, but lets users override it via values.yaml:
# values.yaml
nameOverride: "custom-name"
Fullname
{{- define "my-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
This is the primary resource naming template. It combines the release name and chart name but avoids duplication (if the release is already named my-chart, it won't produce my-chart-my-chart).
Labels
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
{{ include "my-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
Selector Labels
{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
"Why separate labels from selector labels?"
Selector labels are used in spec.selector.matchLabels and spec.template.metadata.labels. These must not change across upgrades — changing them would orphan existing Pods. So they only include the stable identifiers (name + instance).
The full labels include version, chart info, etc. that can safely change between upgrades.
Service Account Name
{{- define "my-chart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "my-chart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
Writing Your Own Helpers
Let's build some practical custom helpers:
Image String Helper
# templates/_helpers.tpl
{{- define "my-chart.image" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- if .Values.image.digest }}
{{- printf "%s@%s" .Values.image.repository .Values.image.digest }}
{{- else }}
{{- printf "%s:%s" .Values.image.repository $tag }}
{{- end }}
{{- end }}
Usage:
containers:
- name: {{ .Chart.Name }}
image: {{ include "my-chart.image" . | quote }}
Conditional Annotations Helper
{{- define "my-chart.annotations" -}}
{{- $annotations := dict -}}
{{- if .Values.commonAnnotations }}
{{- $annotations = merge $annotations .Values.commonAnnotations }}
{{- end }}
{{- if .Values.monitoring.enabled }}
{{- $_ := set $annotations "prometheus.io/scrape" "true" }}
{{- $_ := set $annotations "prometheus.io/port" (.Values.monitoring.port | toString) }}
{{- end }}
{{- if $annotations }}
{{- toYaml $annotations }}
{{- end }}
{{- end }}
Environment Variables Helper
{{- define "my-chart.env" -}}
- name: APP_NAME
value: {{ include "my-chart.fullname" . | quote }}
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- if .Values.existingSecret }}
{{- range .Values.secretKeys }}
- name: {{ . }}
valueFrom:
secretKeyRef:
name: {{ $.Values.existingSecret }}
key: {{ . }}
{{- end }}
{{- end }}
{{- end }}
Multiple Helper Files
Nothing limits you to a single _helpers.tpl. For complex charts, split helpers by purpose:
templates/
├── _helpers.tpl # Core naming and labels
├── _images.tpl # Image-related helpers
├── _validation.tpl # Input validation helpers
├── _env.tpl # Environment variable helpers
├── deployment.yaml
└── service.yaml
Any file starting with _ in templates/ is treated as a partial — not rendered as a manifest.
Library Charts
If you find yourself copying _helpers.tpl across multiple charts, consider creating a library chart. A library chart has type: library in Chart.yaml and contains only named templates:
# Chart.yaml
apiVersion: v2
name: my-common-lib
type: library
version: 1.0.0
# templates/_labels.tpl
{{- define "common.labels" -}}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end }}
Other charts depend on it:
# In your application chart's Chart.yaml
dependencies:
- name: my-common-lib
version: "1.x.x"
repository: "https://your-repo"
And use its templates:
metadata:
labels:
{{- include "common.labels" . | nindent 4 }}
Debugging Named Templates
When a named template produces unexpected output, render it in isolation:
# Render all templates and grep for your template's output
helm template my-release ./my-chart --debug 2>&1 | head -100
# Render just one template file to see how includes resolve
helm template my-release ./my-chart -s templates/deployment.yaml
You can also add a debug template:
# templates/debug.yaml (remove before shipping!)
{{- if .Values.debug }}
# fullname: {{ include "my-chart.fullname" . }}
# labels:
{{ include "my-chart.labels" . | indent 2 }}
# image: {{ include "my-chart.image" . }}
{{- end }}
What's Next?
Named templates keep your charts DRY and maintainable. Combined with the functions and flow control from the previous tutorials, you now have the complete templating toolkit.
Time to shift gears. In the next tutorial, we'll learn about chart dependencies — how to compose complex applications from multiple charts and manage sub-charts.