Helm Helm

Create Your First Helm Chart from Existing Manifests

Turn a Deployment, Service, and ConfigMap into a templated chart, then install and upgrade it.

15 min Beginner

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