How to Write a Helm Chart (Deployment + Service Templates)
Writing a Helm chart means turning fixed Kubernetes manifests into templates that read from values.yaml. Here's how to write a Deployment and Service template with Helm's templating syntax, then render and install them.
From static manifest to template
A plain Deployment hard-codes the image, replica count, and name. A Helm template
replaces those with {{ .Values.* }} placeholders so one chart deploys any config.
Helm renders the template + values.yaml into real Kubernetes YAML at install time.
values.yaml: the inputs
replicas: 3
image: ghcr.io/example/app:1.0.0
port: 8080
templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}
labels:
app: {{ .Chart.Name }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image }}"
ports:
- containerPort: {{ .Values.port }}
templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
spec:
selector:
app: {{ .Chart.Name }} # must match the Deployment's pod labels
ports:
- port: 80
targetPort: {{ .Values.port }}
The templating rules that matter
.Values.xreads fromvalues.yaml;.Chart.Namereads fromChart.yaml.- Quote interpolated strings (
image: "{{ .Values.image }}") so YAML stays valid. - The Service
selectormust match the Deployment's podlabels, or the Service will have no endpoints. - Keep labels consistent (
app: {{ .Chart.Name }}) across both.
Render and install
helm template . # render to plain YAML and check it
helm install myapp . # deploy to the cluster
helm upgrade myapp . --set replicas=5 # change a value, re-deploy
Template helpers and the gotchas that break a chart
Two helpers do most of the work. {{ include "chart.fullname" . }} pulls in a named template from templates/_helpers.tpl, so names and labels stay consistent across every manifest. And {{- toYaml .Values.resources | nindent 12 }} renders a nested block at the right indentation - forgetting nindent is the number-one cause of error converting YAML to JSON on install.
A few rules that save hours:
- Quote values that look like numbers or booleans:
tag: {{ .Values.image.tag | quote }}- an unquoted1.0becomes a float. - Use
{{-and-}}to trim whitespace so blank lines don't break the YAML. - Give every value a default:
{{ .Values.replicas | default 1 }}. - Gate optional blocks with
{{- if .Values.ingress.enabled }}.
Run helm template . to see the rendered YAML before you touch a cluster - it catches indentation and templating bugs that helm install only surfaces as a cryptic error.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Templating a Deployment + Service from values.yaml
- Matching Service selectors to Deployment pod labels
- Rendering with helm template and installing with helm install
FAQ
How do I write a Helm template?
Take a normal Kubernetes manifest and replace the values that vary - image, replicas, name - with {{ .Values.* }} and {{ .Chart.Name }} placeholders. Helm renders them against values.yaml at install time.
Why does my Helm Service have no endpoints?
The Service selector doesn't match the Deployment's pod labels. Keep them identical (e.g. app: {{ .Chart.Name }} on both) so the Service can find the pods.
How do I preview what a Helm chart renders?
Run helm template . to render the templates against your values into plain Kubernetes YAML without installing anything.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →