Your First Helm Chart
Create, install, and manage your very first Helm chart. Deploy an application to Kubernetes using Helm.
Your First Helm Chart
In the previous tutorial, we installed Helm and even deployed an nginx chart from the Bitnami repo. That's cool, but using someone else's chart is like eating at a restaurant — let's learn to cook.
Time to build your own chart from scratch.
Creating a Chart
Helm has a built-in command to scaffold a new chart:
helm create my-first-chart
This creates a directory with everything you need:
my-first-chart/
├── Chart.yaml
├── values.yaml
├── charts/
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ └── tests/
│ └── test-connection.yaml
└── .helmignore
"That's a lot of files for a 'first' chart."
Yeah, helm create gives you the full production template. It's great as a reference, but it can be overwhelming. Let's strip it down and build something simpler from scratch so you understand every piece.
Building a Chart from Scratch
Delete the generated chart and start fresh:
rm -rf my-first-chart
mkdir -p my-first-chart/templates
Step 1: Chart.yaml
Every chart needs a Chart.yaml — it's the chart's identity card.
# my-first-chart/Chart.yaml
apiVersion: v2
name: my-first-chart
description: My very first Helm chart
type: application
version: 0.1.0
appVersion: "1.0.0"
Let's break this down:
apiVersion: v2— Helm 3 charts use v2. Helm 2 charts used v1. Always use v2.name— The chart name. Must match the directory name.description— What this chart does. Shows up in search results.type— Eitherapplication(deployable) orlibrary(reusable helpers, not deployable).version— The chart version. Follows SemVer. Bump this when you change the chart.appVersion— The version of the app being deployed. This is informational — it doesn't affect Helm behavior.
"What's the difference between version and appVersion?"
version is the chart's version (the packaging). appVersion is the application's version (the thing inside). You might update the chart (adding a new value, fixing a template) without changing the app version, and vice versa.
Step 2: A Simple Deployment Template
Create a Deployment template:
# my-first-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
labels:
app: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.containerPort }}
See those {{ }} things? That's Go template syntax. Helm uses it to inject values into your YAML at deploy time. Don't worry about mastering it now — we'll deep-dive into templating in upcoming tutorials.
For now, just know:
{{ .Release.Name }}— The name you give when runninghelm install{{ .Values.something }}— Values fromvalues.yaml(or overridden at install time){{ .Chart.Name }}— The chart name fromChart.yaml
Step 3: A Simple Service Template
# my-first-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-svc
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.containerPort }}
selector:
app: {{ .Release.Name }}
Step 4: Default Values
The values.yaml file defines defaults for all those {{ .Values.xxx }} references:
# my-first-chart/values.yaml
replicaCount: 1
image:
repository: nginx
tag: "1.25"
containerPort: 80
service:
type: ClusterIP
port: 80
This is the beauty of Helm — sensible defaults that work out of the box, but everything is configurable.
Step 5: NOTES.txt (Optional but Nice)
This file prints helpful info after install:
# my-first-chart/templates/NOTES.txt
🎉 {{ .Release.Name }} has been deployed!
To access your application:
kubectl port-forward svc/{{ .Release.Name }}-svc {{ .Values.service.port }}:{{ .Values.service.port }}
Then open http://localhost:{{ .Values.service.port }} in your browser.
Your Chart Structure
Here's what you should have now:
my-first-chart/
├── Chart.yaml
├── values.yaml
└── templates/
├── deployment.yaml
├── service.yaml
└── NOTES.txt
Clean and simple. No bloat.
Installing Your Chart
Let's deploy it:
helm install my-release ./my-first-chart
You should see your NOTES.txt output:
🎉 my-release has been deployed!
To access your application:
kubectl port-forward svc/my-release-svc 80:80
Then open http://localhost:80 in your browser.
Check what got created:
kubectl get all -l app=my-release
NAME READY STATUS RESTARTS AGE
pod/my-release-app-6d4f5b8c9d-xxxxx 1/1 Running 0 10s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/my-release-svc ClusterIP 10.96.45.123 <none> 80/TCP 10s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/my-release-app 1/1 1 1 10s
Boom! One command, and your Deployment + Service are running.
Overriding Values
Now let's see the real power. Deploy the same chart with different settings:
Using --set
helm install my-release-v2 ./my-first-chart \
--set replicaCount=3 \
--set image.tag="1.24" \
--set service.type=NodePort
Using a Values File
For more complex overrides, create a separate file:
# production-values.yaml
replicaCount: 5
image:
repository: nginx
tag: "1.25-alpine"
service:
type: LoadBalancer
port: 80
helm install my-release-prod ./my-first-chart -f production-values.yaml
"Can I combine --set and -f?"
Yes! --set values override -f file values, which override values.yaml defaults. The order of precedence:
values.yaml(lowest priority — defaults)-f custom-values.yaml(overrides defaults)--set key=value(highest priority — overrides everything)
Previewing Before Installing
Not sure what your chart will produce? Use --dry-run:
helm install my-release ./my-first-chart --dry-run --debug
This renders all templates and shows you the final YAML without sending anything to the cluster. Incredibly useful for debugging.
You can also use helm template for the same purpose:
helm template my-release ./my-first-chart
The difference? helm template is purely local — it doesn't even need a cluster connection. --dry-run validates against the actual cluster.
Upgrading a Release
Made changes to your chart? Upgrade the release:
helm upgrade my-release ./my-first-chart --set replicaCount=3
Helm compares the new rendered templates against the current state and applies only the differences. Smart.
Checking Release Status
# List all releases
helm list
# Get details about a specific release
helm status my-release
# See what values are in use
helm get values my-release
# See the rendered manifests
helm get manifest my-release
Cleaning Up
helm uninstall my-release
helm uninstall my-release-v2
All resources created by the releases are deleted. Clean and tidy.
What's Next?
You just built and deployed your first Helm chart from scratch! Here's what you learned:
- Creating a chart with
Chart.yaml,values.yaml, and templates - Go template basics (
{{ .Values.xxx }},{{ .Release.Name }}) - Installing, upgrading, and uninstalling releases
- Overriding values with
--setand-f - Previewing templates with
--dry-runandhelm template
In the next tutorial, we'll take a deeper look at chart structure — understanding every file and directory, and the conventions that make charts maintainable. Let's go!