Skip to content
daniel@cosenza:~/blog
SRE & DevOpsHow-To April 27, 2026 3 min read

How to Write a Helm Chart from Scratch

A complete walkthrough building a Helm chart for a simple application — templates, values, and the conventions that make a chart genuinely reusable rather than a one-off wrapper around raw YAML.

A Helm chart packages a Kubernetes application’s manifests as reusable, parameterized templates — this walks through building one for a simple web application from scratch.

Step 1: scaffold a new chart

helm create mychart

This generates a conventional chart directory structure: Chart.yaml (metadata), values.yaml (default configuration), and a templates/ directory with example manifests already following Helm’s conventions.

Step 2: fill in chart metadata

# Chart.yaml
apiVersion: v2
name: mychart
description: A Helm chart for my application
version: 0.1.0
appVersion: "1.0.0"

version tracks the chart’s own version (incrementing as you change the templates); appVersion tracks the version of the application the chart deploys — these are deliberately independent, since a chart’s packaging can change without the application itself changing, and vice versa.

Step 3: define default configuration values

# values.yaml
replicaCount: 2
image:
  repository: myapp
  tag: "1.0.0"
service:
  port: 80
resources:
  requests:
    cpu: 100m
    memory: 128Mi

values.yaml is the chart’s public configuration surface — anything a user of the chart should be able to customize belongs here, referenced from templates rather than hardcoded.

Step 4: template the Deployment manifest

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-mychart
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels: {app: {{ .Release.Name }}}
  template:
    metadata:
      labels: {app: {{ .Release.Name }}}
    spec:
      containers:
        - name: mychart
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          resources: {{ toYaml .Values.resources | nindent 12 }}

{{ .Values.* }} references pull from values.yaml; {{ .Release.Name }} is a built-in variable set to whatever name the chart is installed under — using it instead of a hardcoded name lets the same chart be installed multiple times under different release names without colliding.

Step 5: template the Service manifest

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-mychart
spec:
  selector: {app: {{ .Release.Name }}}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 8080

Step 6: validate the templates render correctly before installing

helm template mychart ./mychart

This renders the final Kubernetes YAML the chart would produce, without actually installing anything — the fastest way to catch a templating mistake before it becomes a failed helm install.

Step 7: lint the chart for common mistakes

helm lint ./mychart

helm lint checks for structural problems and common conventions the chart might be violating, beyond just whether the templates render without error.

Step 8: install the chart

helm install myrelease ./mychart --set replicaCount=3

--set overrides specific values from the command line without needing a separate values file — useful for quick overrides, though a dedicated values file (-f custom-values.yaml) is generally more maintainable for anything beyond a one-off change.

Step 9: package the chart for distribution

helm package ./mychart

This produces a versioned .tgz archive suitable for uploading to a chart repository, the same distribution format used by public charts like kube-prometheus-stack.

Why separating values from templates is the entire point

The distinction between values.yaml (what a user can configure) and templates/ (how those values get woven into actual Kubernetes manifests) is what makes a chart genuinely reusable across different environments and deployments, rather than being a one-off YAML file with find-and-replace placeholders. Getting this separation right — deciding what genuinely belongs in values.yaml versus what should stay fixed in the templates — is the actual design skill in writing a good chart, more so than Helm’s templating syntax itself.