If you've ever hand-written Kubernetes YAML files for a real application, you know the pain. A simple web app needs a Deployment, a Service, an Ingress, a ConfigMap, maybe a HorizontalPodAutoscaler — and every environment (dev, staging, production) needs slightly different values. Multiply that by ten microservices and you're drowning in copy-pasted YAML.
Helm solves this. It's the package manager for Kubernetes — think apt or brew but for cluster workloads. Instead of managing raw YAML, you work with charts: templated, versioned, reusable packages that install with a single command.
Why Helm Matters
Without Helm, deploying an app to Kubernetes means maintaining a growing pile of YAML files. Need to change the replica count for staging? Edit the file. Need to add an environment variable? Edit the file. Want to roll back? Hope you kept a copy of the old YAML.
With Helm:
- Templating — one chart, many environments. Values change, templates stay the same.
- Releases — every install is tracked. Rollback with one command.
- Package distribution — share charts via OCI registries or chart repositories.
- Lifecycle management — hooks for pre-install, post-upgrade, and rollback operations.
Installing Helm
On macOS with Homebrew:
brew install helm
On Linux:
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Verify the installation:
helm version
# version.BuildInfo{Version:"v3.16.x", ...}
Helm 3 is the current major version. If you see references to Tiller, that's Helm 2 — ignore it. Helm 3 removed the server-side component entirely and talks directly to the Kubernetes API.
Installing Your First Chart
The fastest way to get started is installing a chart from a public repository. Let's deploy NGINX:
# Add the Bitnami repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Install NGINX
helm install my-nginx bitnami/nginx \
--namespace demo --create-namespace \
--set service.type=ClusterIP
# Check the release
helm list -n demo
kubectl get pods -n demo
That single helm install command created a Deployment, a Service, and all the supporting resources. No YAML files to maintain. To customize values, you can pass them inline with --set or use a values file.
Understanding Chart Structure
Create a new chart to see how it works:
helm create my-app
tree my-app/
The generated structure:
my-app/
├── Chart.yaml # Chart metadata (name, version, description)
├── values.yaml # Default configuration values
├── charts/ # Dependencies (sub-charts)
├── templates/
│ ├── deployment.yaml # Kubernetes resource templates
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── serviceaccount.yaml
│ ├── _helpers.tpl # Template helpers (partial templates)
│ ├── NOTES.txt # Post-install message
│ └── tests/
│ └── test-connection.yaml
└── .helmignore # Files to exclude when packaging
The key files are Chart.yaml (who am I?), values.yaml (what are my defaults?), and everything under templates/ (what do I create in the cluster?).
Templates: Where the Magic Happens
Helm templates are Kubernetes YAML files with Go template expressions embedded. Here's a simplified Deployment template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
env:
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
Every {{ .Values.xxx }} expression pulls from values.yaml or user overrides. This is the core idea: the template describes the shape of your resources, and values fill in the specifics.
Customizing with Values Files
Instead of passing dozens of --set flags, create per-environment values files:
# values-staging.yaml
replicaCount: 1
image:
repository: myapp
tag: staging-latest
pullPolicy: Always
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
# values-production.yaml
replicaCount: 3
image:
repository: myapp
tag: v1.2.0
pullPolicy: IfNotPresent
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
Deploy with the file:
helm install myapp ./my-app \
-f values-production.yaml \
-n production --create-namespace
Helm merges values in order: values.yaml (defaults) → -f files (left to right) → --set flags (highest priority). This layering lets you keep a sane base and override only what differs per environment.
Releases and Rollbacks
Every helm install creates a release — a named deployment with a revision history stored as Kubernetes Secrets.
# Upgrade a release
helm upgrade myapp ./my-app -f values-production.yaml
# See release history
helm history myapp
# Rollback to revision 2
helm rollback myapp 2
# Uninstall completely
helm uninstall myapp -n production
Rollbacks are instant because Helm stores the full manifest of every revision. This alone makes Helm worth adopting — it gives you deployment history without needing an external tool.
Dependency Management
Charts can depend on other charts. A common pattern is depending on Redis or PostgreSQL as a sub-chart:
# Chart.yaml
dependencies:
- name: redis
version: "18.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
# Update dependencies
helm dependency update ./my-app
# This downloads the redis chart into charts/
The condition field lets you toggle dependencies per environment. Production might use an external Redis instance while dev uses the bundled sub-chart.
Packaging and Sharing
Package your chart for distribution:
# Lint first
helm lint ./my-app
# Package as a .tgz archive
helm package ./my-app
# Creates my-app-0.1.0.tgz
# Push to an OCI registry (GitHub Container Registry, ECR, etc.)
helm push my-app-0.1.0.tgz oci://ghcr.io/myorg/charts
OCI-based registries are now the standard for chart distribution. No need to maintain a separate chart repository server — your container registry does double duty.
Debugging Helm Charts
When things go wrong, these commands save you time:
# Render templates locally without installing (dry-run)
helm template myapp ./my-app -f values-production.yaml
# Dry-run against the cluster (validates against API server)
helm install myapp ./my-app --dry-run --debug
# See what would change in an upgrade
helm diff upgrade myapp ./my-app -f values-production.yaml
# (requires helm-diff plugin: helm plugin install https://github.com/databus23/helm-diff)
The helm template command is invaluable during development — it shows you the exact Kubernetes YAML that will be applied, without touching the cluster. Pair it with helm diff for upgrades and you'll rarely be surprised by what lands in your cluster.
Summary
Helm transforms Kubernetes deployment from ad-hoc YAML management into a structured, repeatable process:
- Charts are templated, versioned packages for your applications.
- Values separate configuration from structure, enabling multi-environment deployments.
- Releases track every deployment with built-in rollback.
- OCI registries serve as your chart distribution mechanism.
Start by converting one of your existing YAML deployments into a Helm chart. Replace hardcoded values with template expressions, create per-environment value files, and use helm upgrade for deployments. Once you experience rollback with helm history and helm rollback, you'll never go back to managing raw YAML.