If you have spent any time with Kubernetes, you already know the feeling. You want to deploy something simple, say a database or an Nginx server, and suddenly you are juggling a Deployment, a Service, a ConfigMap, maybe a Secret, and an Ingress. That is five YAML files for one app. Now multiply that by every environment you run (dev, staging, production) where only a handful of values actually change, and the copy-paste mess starts to feel unmanageable.
Helm solves exactly this. It is the package manager for Kubernetes, often described as “apt or yum, but for clusters.” Instead of hand-writing and tracking dozens of manifests, you install a single packaged unit called a chart, override only the values you care about, and let Helm handle the rest. Upgrades, rollbacks, and clean uninstalls become single commands.
This tutorial is for developers, sysadmins, and DevOps engineers who already have a working Kubernetes cluster and want to stop managing raw YAML by hand. By the end you will have installed Helm, deployed a real application from a public chart, customized it with your own values, upgraded and rolled it back, and built a small chart of your own from scratch.
If you do not have a cluster yet, set one up first using my earlier guide on installing single-node Kubernetes. If your LoadBalancer services never get an external IP, my MetalLB guide will help.
Conceptual Overview
Before touching commands, let us get the vocabulary straight. Helm has only a few core concepts, and once they click everything else makes sense.
A chart is a package. It is a folder (or a compressed .tgz file) containing templated Kubernetes manifests plus some metadata. Think of it as a blueprint for an application. A chart for PostgreSQL knows how to create the StatefulSet, Service, and Secret that PostgreSQL needs.
A release is one installed instance of a chart in your cluster. If you install the same Nginx chart three times with three different names, you have three releases. This is important: charts are reusable templates, releases are the running results.
A values file is where customization happens. Charts ship with sensible defaults in a values.yaml file, but you override anything you like (replica count, image tag, resource limits) with your own values without ever editing the chart itself.
A repository is a place where charts are hosted, similar to an apt repository. You add a repo, then install charts from it by name.
One more thing worth knowing: modern Helm (version 3 and later) runs entirely client-side and talks directly to the Kubernetes API. Older tutorials mention a server-side component called Tiller. That was removed years ago. If you read a guide that tells you to helm init or install Tiller, it is outdated, so ignore it.
Prerequisites
To follow along you will need:
- A working Kubernetes cluster (single-node is fine for learning).
kubectlinstalled and configured, with a valid kubeconfig that can reach your cluster. Test it withkubectl get nodes.- An Ubuntu machine (20.04, 22.04, or 24.04) where you run the commands. This can be your control-plane node or a separate workstation.
- Basic comfort with the Linux command line and a rough idea of what Kubernetes Deployments and Services are. If you want a refresher, see my post on understanding Kubernetes objects.
Confirm your cluster is reachable before going further:
kubectl get nodes
You should see at least one node in Ready state:
NAME STATUS ROLES AGE VERSION
master-1 Ready control-plane 12d v1.30.2
Step 1: Install Helm on Ubuntu
There are several ways to install Helm, but the cleanest on Ubuntu is the official apt repository, because it keeps Helm updated through the normal apt upgrade flow.
First, add the signing key and the repository:
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
Then update and install:
sudo apt-get update
sudo apt-get install helm
Verify the installation:
helm version
You should see something like:
version.BuildInfo{Version:"v3.15.2", GitCommit:"...", GitTreeState:"clean", GoVersion:"go1.22.4"}
The key thing to confirm is that the version starts with v3 or higher. That tells you the modern, Tiller-free Helm is running.
Helm reads the same kubeconfig that kubectl uses, so if kubectl already works, Helm will too. No extra wiring needed.
Step 2: Add a Chart Repository
Out of the box, Helm does not know about any charts. You add a repository first. We will use Bitnami, a well-maintained public repo with hundreds of production-ready charts.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
The repo update command refreshes your local cache of what charts are available, similar to apt update. Run it whenever you want the latest chart versions.
You can search the repo to see what is on offer:
helm search repo bitnami/nginx
NAME CHART VERSION APP VERSION DESCRIPTION
bitnami/nginx 18.1.6 1.27.0 NGINX Open Source is a web server...
Notice the two version columns. Chart version is the version of the Helm package itself, while app version is the version of the software inside it. They move independently, so do not confuse them.
Step 3: Install Your First Release
Let us deploy Nginx. To keep things tidy, we will put it in its own namespace.
kubectl create namespace web
helm install my-web bitnami/nginx --namespace web
Here my-web is the release name you choose, and bitnami/nginx is the chart. Helm prints a summary and some usage notes pulled from the chart itself.
Check what you just created:
helm list --namespace web
NAME NAMESPACE REVISION STATUS CHART APP VERSION
my-web web 1 deployed nginx-18.1.6 1.27.0
Notice REVISION 1. Every time you change a release, Helm bumps this number, which is what makes rollbacks possible later.
Now look at the actual Kubernetes objects Helm created on your behalf:
kubectl get all --namespace web
You will see a Deployment, a ReplicaSet, a Pod, and a Service, all from a single command. That is the whole point. You did not write a single line of YAML.
Step 4: Customize with Values
Defaults are fine for a demo, but real deployments need tweaking. The first thing to do with any chart is read its available values:
helm show values bitnami/nginx | less
This dumps every configurable option with comments. It is long, but it is the source of truth for what you can change.
Say you want two replicas instead of one, and you want the service exposed as LoadBalancer. Create a small values file with only the keys you care about:
# my-web-values.yaml
replicaCount: 2
service:
type: LoadBalancer
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
The trick to understand here is that you only override what you need. Helm merges your file on top of the chart’s defaults, so everything you leave out keeps its default value. You never have to copy the whole values.yaml.
Apply the changes with helm upgrade:
helm upgrade my-web bitnami/nginx --namespace web --values my-web-values.yaml
Check the release again:
helm list --namespace web
The revision is now 2. Confirm the pod count went up:
kubectl get pods --namespace web
NAME READY STATUS RESTARTS AGE
my-web-7c9d8f6b4d-2x9pq 1/1 Running 0 40s
my-web-7c9d8f6b4d-kf8mn 1/1 Running 0 40s
A handy habit: a single command can install or upgrade depending on whether the release already exists. This is great for scripts and CI pipelines:
helm upgrade --install my-web bitnami/nginx --namespace web --values my-web-values.yaml
Step 5: Roll Back a Bad Change
Mistakes happen. Suppose your latest upgrade broke something. Helm keeps the history of every revision, so going back is painless.
View the history:
helm history my-web --namespace web
REVISION UPDATED STATUS CHART DESCRIPTION
1 Tue Jun 17 09:10:22 2026 superseded nginx-18.1.6 Install complete
2 Tue Jun 17 09:18:41 2026 deployed nginx-18.1.6 Upgrade complete
To return to revision 1:
helm rollback my-web 1 --namespace web
Helm creates a new revision (number 3) that matches the state of revision 1. It does not delete history, it moves forward to a known-good state. Verify with helm history again. This safety net is one of the biggest reasons teams adopt Helm in production.
Step 6: Inspect Before You Apply
When you are unsure what a chart will actually create, render it locally without touching the cluster:
helm template my-web bitnami/nginx --values my-web-values.yaml --namespace web
This prints the fully rendered YAML to your terminal so you can review it. A related flag works on real commands too:
helm upgrade my-web bitnami/nginx --namespace web --values my-web-values.yaml --dry-run
The --dry-run flag sends the request to the cluster for validation but does not apply it. Use these two regularly. They turn “I hope this works” into “I know what this does.”
Step 7: Build Your Own Chart
Public charts are great, but eventually you will want to package your own app. Helm scaffolds a complete starter chart for you:
helm create myapp
This creates a myapp/ directory:
myapp/
├── Chart.yaml # metadata: name, version, description
├── values.yaml # default configuration values
├── charts/ # subcharts (dependencies) go here
└── templates/ # the templated Kubernetes manifests
├── deployment.yaml
├── service.yaml
├── _helpers.tpl
└── ...
Open templates/deployment.yaml and you will see Go templating in action. Instead of a hardcoded image, you get something like:
image: ":"
That `` pulls its value straight from values.yaml. To point the chart at your own image, edit values.yaml:
image:
repository: nginx
tag: "1.27"
replicaCount: 1
service:
type: ClusterIP
port: 80
Before installing, lint the chart to catch obvious errors:
helm lint ./myapp
==> Linting ./myapp
1 chart(s) linted, 0 chart(s) failed
Then install it from the local directory:
helm install demo ./myapp --namespace web
You now have a release running from a chart you wrote. From here you can version it in Chart.yaml, package it with helm package ./myapp, and host it in a repository for your whole team to use.
Common Mistakes and Troubleshooting
“Error: Kubernetes cluster unreachable.” Helm cannot find or reach your cluster. This is almost always a kubeconfig issue, not a Helm issue. Run kubectl get nodes first. If that fails, fix your kubeconfig before blaming Helm.
“cannot re-use a name that is still in use.” You tried to helm install a release name that already exists. Either pick a new name or use helm upgrade instead. This is exactly why helm upgrade --install is so popular.
Pasting the whole values.yaml and editing it. Beginners often copy the entire default values file and tweak a couple of lines. This works, but it means you inherit every default the chart author later changes. Keep your override file minimal and let the chart own its defaults.
Forgetting the namespace. Helm release names are scoped per namespace, and so is helm list. If helm list shows nothing but you are sure you installed something, you are probably looking at the wrong namespace. Add --namespace web or use helm list --all-namespaces.
Confusing chart version and app version. When someone reports “we run Nginx 1.27,” that is the app version. When you pin a deployment for reproducibility, pin the chart version with --version, for example helm install my-web bitnami/nginx --version 18.1.6. Pinning avoids surprise upgrades when the repo publishes a new chart.
A stuck release in pending-upgrade status. This happens when an upgrade is interrupted. Check helm history, then roll back to the last good revision with helm rollback.
Best Practices
A few habits will keep your Helm usage clean and production-safe.
Always pin chart versions in real environments. Use --version so a helm upgrade six months from now does not silently pull a brand new chart with breaking changes. Reproducibility beats convenience.
Keep values files in version control. Your my-web-values.yaml is the real description of how your app is configured. Commit it to Git alongside your code. Never store secrets in plain values files, though. For sensitive data, pull from Kubernetes Secrets or a tool like Sealed Secrets, and see my guide on creating Kubernetes secrets.
Use one values file per environment. A common layout is values-dev.yaml, values-staging.yaml, and values-prod.yaml. You can even stack them: helm upgrade ... -f values-common.yaml -f values-prod.yaml, where the later file wins on conflicts.
Run helm template or --dry-run in CI. Catch rendering errors before they reach the cluster. It is cheap insurance.
Set resource requests and limits. Many charts leave these empty by default, which can let a single app starve a node. Always define them in your values, as shown earlier.
Clean up releases you no longer need. A simple helm uninstall my-web --namespace web removes every object the release created, no orphaned ConfigMaps left behind. This clean teardown is far safer than kubectl delete on individual objects you might forget.
Conclusion
You have covered the full Helm workflow end to end. You installed Helm on Ubuntu, added a repository, deployed a real application as a release, customized it with a minimal values file, upgraded it, rolled it back to a known-good state, inspected changes before applying them, and built a chart of your own from scratch.
That is genuinely most of what you need for day-to-day Kubernetes work. Helm turns a pile of fragile YAML into versioned, repeatable, reversible deployments, which is exactly what you want when real users depend on your cluster.
For next steps, try packaging your chart with helm package and hosting it in a private repository so your team can install it by name. Explore chart dependencies, where one chart pulls in others (a web app that automatically deploys its own Redis, for example). And once you are comfortable, look into GitOps tools like Argo CD or Flux, which deploy your Helm charts automatically whenever you push to Git. Helm is the foundation that all of those build on, and you now have it solid.