If you’ve ever scaffolded a Helm chart with helm create, you’ve probably noticed a mysterious file called _helpers.tpl sitting inside the /templates folder — and then completely ignored it.

Don’t worry, most people do. But once you understand what it does, you’ll wonder how you ever managed without it.

In this guide, we’re going to demystify _helpers.tpl from the ground up, with a hands-on example you can follow along with.

What You’ll Learn

By the end of this tutorial, you’ll know how to:

  • Understand what _helpers.tpl is and why it exists
  • Define reusable template blocks (called named templates or partials)
  • Pull in dynamic values from chart.yaml and values.yaml
  • Include those blocks across multiple resource templates
  • Validate and install a working Helm chart end to end

The Problem It Solves

Before we dive into _helpers.tpl, let’s understand why it exists.

When you write Helm templates for Kubernetes resources — Deployments, Services, ConfigMaps, and so on — you’ll usually want consistent labels on all of them. Something like:

1
2
3
4
app: my-app
version: 1.0.0
managed-by: Helm
env: production

The natural instinct is to copy and paste this block into every template file. And that works… until you need to change something. Now you’re hunting through five files making the same edit, hoping you didn’t miss one.

There’s a better way. Define those labels once in _helpers.tpl, and reference them from all your other templates. Change it in one place — it updates everywhere.

Think of it like a function in programming. Instead of duplicating logic, you write it once and call it wherever you need it.

What Exactly Is _helpers.tpl?

_helpers.tpl is a special file that lives in your chart’s /templates directory. It contains reusable snippets of template code called named templates (sometimes called partials).

Key insight: Helm treats any file whose name starts with _ as a helper file. It won’t try to render it directly as a Kubernetes manifest. It’s purely there to be called by your other templates.

Here’s the basic anatomy of a named template:

1
2
3
4
{{- define "my-chart.labels" -}}
app: {{ .Release.Name }}
managed-by: Helm
{{- end }}
  • define gives the template a name ("my-chart.labels")
  • Everything between define and end is the reusable content
  • The {{-` and `-}} trim whitespace (we’ll come back to why that matters)

To use this block in another template, you call it with include:

1
2
3
metadata:
labels:
{{- include "my-chart.labels" . | nindent 4 }}

The . passes through the current context (so values from chart.yaml and values.yaml are accessible), and nindent 4 makes sure the output is indented by 4 spaces — keeping your YAML valid.

The diagram below shows how this all connects:

1
2
3
4
5
6
7
8
9
10
_helpers.tpl                   deployment.yaml / service.yaml
┌─────────────────────┐ ┌──────────────────────────────┐
│ define "labels" │──────▶ │ include "labels" . | nindent│
│ app: ... │ │ (labels rendered here) │
│ env: ... │ └──────────────────────────────┘
└─────────────────────┘

│ reads values from

chart.yaml / values.yaml

Hands-On Example: _helpers.tpl in Action

Let’s build a minimal Helm chart that demonstrates exactly how this works.

What We’re Building

  • A _helpers.tpl file that defines a shared label block
  • Labels that pull values dynamically from chart.yaml and values.yaml
  • A deployment.yaml and service.yaml that both use those labels
  • Full validation using helm template

Step 1: Create the Helm Boilerplate

Start by scaffolding a new Helm chart:

1
helm create web-app

This creates a bunch of default files. We’re going to replace them with our own, so clear out the templates directory and the default values file:

1
2
rm -rf web-app/templates/*
rm web-app/values.yaml

Step 2: Create values.yaml

This file holds the configurable values for our chart. Run:

1
2
3
4
5
6
7
8
9
10
11
12
13
cat > web-app/values.yaml << 'EOF'
replicaCount: 1

image:
repository: nginx
tag: "1.25"

service:
type: ClusterIP
port: 80

env: stage
EOF

Notice the env field at the bottom — we’re going to use that as a label on our Kubernetes resources.

Step 3: Write the _helpers.tpl File

Now let’s create the star of the show:

1
2
3
4
5
6
7
8
cat > web-app/templates/_helpers.tpl << 'EOF'
{{- define "web-app.labels" -}}
app: {{ .Release.Name }}
version: {{ .Chart.AppVersion }}
managed-by: Helm
env: {{ .Values.env }}
{{- end }}
EOF

Let’s break down what each label does:

Label Source Example Value
app Helm release name (set during installation) web-app
version appVersion field in Chart.yaml 1.16.0
managed-by Static value set by Helm Helm
env env field in values.yaml stage

Note: The {{-` at the start and `-}} at the end strip leading/trailing whitespace. This is important when you include this block inside YAML, as unexpected blank lines can break things.

Step 4: Create the Deployment Template

Now let’s create a deployment.yaml that uses our helper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cat > web-app/templates/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
{{- include "web-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
{{- include "web-app.labels" . | nindent 8 }}
spec:
containers:
- name: nginx
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 80
EOF

You’ll notice include "web-app.labels" appears twice — once for the Deployment’s own metadata labels, and once for the Pod template’s labels. Both use the same helper, and both stay in sync automatically.

The nindent 4 and nindent 8 simply control how many spaces to indent the output so the YAML remains valid.

Step 5: Create the Service Template

Same idea for the service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat > web-app/templates/service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
labels:
{{- include "web-app.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
app: {{ .Release.Name }}
ports:
- protocol: TCP
port: {{ .Values.service.port }}
targetPort: 80
EOF

Step 6: Validate the Rendered Output

Before installing anything, use helm template to preview what Helm will actually generate:

1
helm template web-app ./web-app

You should see both the Deployment and Service rendered, each with the labels block filled in:

1
2
3
4
5
labels:
app: web-app
version: 1.16.0
managed-by: Helm
env: stage

Both resources get the exact same labels — defined in one place, rendered everywhere. If you ever need to change managed-by to helm-managed, you change it in _helpers.tpl and it updates everywhere automatically.

Step 7: Install the Chart

If everything looks right, go ahead and install:

1
helm install web-app ./web-app

Expected output:

1
2
3
4
5
6
NAME: web-app
LAST DEPLOYED: ...
NAMESPACE: default
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete

Now verify the labels were applied correctly:

1
kubectl get deploy web-app --show-labels

You should see:

1
2
NAME      READY   UP-TO-DATE   AVAILABLE   AGE   LABELS
web-app 1/1 1 1 30s app=web-app,env=stage,managed-by=Helm,version=1.16.0

Step 8: Clean Up

When you’re done experimenting:

1
helm uninstall web-app

What Else Can You Put in _helpers.tpl?

Labels are the most common use case, but you can define any reusable template logic:

  • Image name construction — combine repository and tag into a single string
  • Resource name prefixes — ensure consistent naming across resources
  • Selector labels — separate from regular labels so selectors stay stable across upgrades
  • Annotations — reusable annotation blocks, just like labels

Here’s a quick example of a more advanced helper that builds an image string:

1
2
3
{{- define "web-app.image" -}}
{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}
{{- end }}

Then in your Deployment:

1
image: {{ include "web-app.image" . }}

Things to Keep in Mind

  • Keep helpers focused. _helpers.tpl is for reusable template logic, not application configuration. Don’t use it as a dumping ground for complex business rules.
  • Don’t over-engineer. Helm’s templating is powerful, but deeply nested or recursive partials are hard to debug. Keep things readable.
  • Any file starting with _ works. You can split helpers across multiple files like _labels.tpl, _images.tpl, etc. if your chart grows large. Helm will pick them all up.

Conclusion

_helpers.tpl is one of those features that seems optional right up until you find yourself editing the same label block in five different files. Once you start using it, you’ll never go back.

To recap what we covered:

  • _helpers.tpl holds reusable named templates that can be referenced from any other template in your chart
  • Use define to create a named template, and include to use it
  • Dynamic values from chart.yaml and values.yaml are fully accessible inside helpers
  • nindent keeps your YAML indentation valid when including blocks
  • Files starting with _ are never rendered as Kubernetes manifests — they exist only to be called

Give it a try on your next Helm chart — your future self will thank you.

Happy Coding