Create Your First Helm Chart from Existing Manifests
Turn a Deployment, Service, and ConfigMap into a templated chart, then install and upgrade it.
The fastest way to learn Helm templating is to take manifests you already understand and templatize them one field at a time. This guide starts from a plain Deployment, Service, and ConfigMap, scaffolds a chart, and replaces the fields that should vary with values.
Scaffold a chart and clear out the example
helm create webapp
cd webapp
rm -rf templates/tests templates/hpa.yaml templates/ingress.yaml \
templates/serviceaccount.yaml templates/_helpers.tpl
The generated templates/deployment.yaml and templates/service.yaml are a working example, but for learning it’s clearer to write your own from scratch. Remove their contents (leave the files) and empty values.yaml:
: > values.yaml
Add your own manifests
Write plain Kubernetes YAML first, exactly as you would without Helm:
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-webapp
labels:
app: my-webapp
spec:
replicas: 3
selector:
matchLabels:
app: my-webapp
template:
metadata:
labels:
app: my-webapp
spec:
containers:
- name: my-webapp
image: myorg/webapp:1.0
envFrom:
- configMapRef:
name: my-webapp-config
ports:
- containerPort: 8080
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-webapp
spec:
selector:
app: my-webapp
ports:
- port: 80
targetPort: 8080
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-webapp-config
data:
CUSTOM_HEADER: "this app was deployed with Helm"
Notice the same string, my-webapp, repeated across the name, the label, the selector, and the ConfigMap reference. That repetition is exactly what makes hand-maintained multi-environment manifests error-prone: rename the app once and you have four places to catch.
Replace the repeated values with a template value
Introduce one value and reference it everywhere the name appears:
# values.yaml
appName: my-webapp
# templates/deployment.yaml (excerpt)
metadata:
name: {{ .Values.appName }}
labels:
app: {{ .Values.appName }}
spec:
selector:
matchLabels:
app: {{ .Values.appName }}
template:
metadata:
labels:
app: {{ .Values.appName }}
spec:
containers:
- name: {{ .Values.appName }}
envFrom:
- configMapRef:
name: {{ .Values.appName }}-config
Do the same in service.yaml and configmap.yaml wherever my-webapp appears. .Values is populated from values.yaml, so a single change there now renames every resource consistently.
Templatize the image and the ConfigMap’s data
Split the image into its own values so the tag can change independently of the repository:
# values.yaml (add)
image:
repository: myorg/webapp
tag: "1.0"
- name: {{ .Values.appName }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
And the ConfigMap’s content:
# values.yaml (add)
config:
customHeader: "this app was deployed with Helm"
# templates/configmap.yaml
data:
CUSTOM_HEADER: {{ .Values.config.customHeader | quote }}
The | quote filter wraps the value in double quotes, which matters once environment-specific values start containing spaces or special characters. A good rule for naming your values: mirror the manifest’s own structure (config.customHeader maps to a ConfigMap’s data.CUSTOM_HEADER), so anyone reading the template can guess where a value comes from.
Preview before installing
Two commands render the chart without touching a cluster:
helm template ./webapp
helm install my-webapp ./webapp --dry-run --debug
helm template renders locally and needs no cluster at all. --dry-run --debug on install additionally validates against the cluster’s API without persisting anything. Run one of these after every template change; a typo in {{ .Values.appname }} (wrong case) silently renders as empty rather than erroring, and this is the fastest way to catch it.
Install and confirm
helm install my-webapp ./webapp
kubectl get all -l app=my-webapp
my-webapp here is the release name, and ./webapp is the chart directory. Nothing about installing a hand-written chart differs from installing a published one.
Change a value and upgrade
Edit values.yaml (bump image.tag, change config.customHeader, whatever you’re testing) and apply the change:
helm upgrade my-webapp ./webapp
helm list
helm list now shows revision 2. Every upgrade reapplies the whole chart with the current values, so this is also how you’d deploy a new version day to day.
Add a NOTES.txt
# templates/NOTES.txt
Your release "{{ .Release.Name }}" has been deployed.
To access {{ .Values.appName }}, run:
kubectl port-forward svc/{{ .Values.appName }} 8080:80
.Release.Name is a built-in value Helm always provides, alongside .Chart and .Release.Namespace; you don’t set it yourself. Run helm upgrade my-webapp ./webapp again and this text prints to the terminal, with the port-forward command already filled in for whoever’s watching.
Popular Guides
-
Helm Chart Anatomy: Charts, Values, Templates, and Releases The folder structure every chart shares, and the three concepts that make Helm more than a YAML bundler. -
One Chart, Many Environments: Values Files for Dev, Staging, and Prod Override just what differs between environments, and understand the order values are merged in. -
Manage Releases: Install, Upgrade, Rollback, and Uninstall The commands that cover a release's whole lifecycle, and how revision history makes rollback possible.