Helm: The Package Manager
- Templating Engine: Helm eliminates YAML duplication by allowing you to define generic templates and inject environment-specific values at install or upgrade time.
- Charts and Releases: A "Chart" is the package of templates and default values, while a "Release" is a specific deployed instance of that chart running in your cluster. You can have multiple releases from the same chart.
- Lifecycle Management: Helm provides simple commands (
install,upgrade,rollback,uninstall) to manage the entire lifecycle of complex applications, including tracking revision history. - Hooks: Helm hooks let you run Jobs or other resources at specific points in a release lifecycle (pre-install, post-upgrade, etc.) for tasks like database migrations.
- Dependency Management: Charts can depend on other charts (subcharts), allowing you to compose complex applications from reusable building blocks.
- OCI Registry Support: Helm 3.8+ supports storing and distributing charts via OCI-compliant container registries like Docker Hub, ECR, and GHCR.
Kubernetes manifests (YAML) can get repetitive. If you want to deploy the same app to dev, staging, and prod, you don't want to copy-paste the file 3 times just to change the imageTag.
Helm solves this by treating Kubernetes YAMLs as Templates.
Visualizing Helm
Helm takes a Template (logic) and combines it with Values (config) to generate the final Manifest (output).
values.yaml
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
template:
spec:
containers:
- image: my-app:1.2.0
Key Concepts
1. Chart
A package of pre-configured Kubernetes resources. A chart is a directory (or a .tgz archive) containing a specific file structure that Helm understands.
2. Release
An instance of a chart running in a Kubernetes cluster. You can install the same chart multiple times (e.g., mysql-dev and mysql-prod). Each installation is a separate release with its own revision history.
3. Repository
A place where charts are stored and shared (like Docker Hub, but for charts).
- Artifact Hub: The official search engine for public charts at artifacthub.io.
- OCI Registries: Helm 3.8+ supports pushing/pulling charts to any OCI-compliant registry.
Chart Structure
Every Helm chart follows a standard directory layout:
mychart/
Chart.yaml # Chart metadata (name, version, dependencies)
Chart.lock # Lock file for pinned dependency versions
values.yaml # Default configuration values
values.schema.json # Optional JSON schema to validate values
templates/ # Kubernetes manifest templates
deployment.yaml
service.yaml
ingress.yaml
hpa.yaml
_helpers.tpl # Template partials (shared snippets)
NOTES.txt # Post-install instructions shown to user
tests/
test-connection.yaml
charts/ # Dependency charts (subcharts)
crds/ # Custom Resource Definitions (applied before templates)
Chart.yaml
The Chart.yaml file contains metadata about the chart:
apiVersion: v2
name: my-web-app
description: A Helm chart for deploying my web application
type: application # "application" or "library"
version: 1.2.0 # Chart version (SemVer)
appVersion: "3.5.1" # Version of the app being deployed
keywords:
- web
- nginx
maintainers:
- name: Platform Team
email: platform@example.com
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
The version field is the chart version and should be incremented on every change to the chart. The appVersion field is informational and reflects the version of the application the chart deploys.
values.yaml
The values.yaml file defines the default configuration. Users override these values at install or upgrade time:
# values.yaml
replicaCount: 2
image:
repository: my-registry.example.com/my-web-app
tag: "3.5.1"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: nginx
host: app.example.com
tls:
enabled: false
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
postgresql:
enabled: true
auth:
postgresPassword: changeme
database: myapp
Template Syntax
Helm templates use the Go template language with additional Helm-specific functions from the Sprig library.
Basic Value Injection
Access values from values.yaml with {{ .Values.key }}:
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
Built-in Objects
Helm provides several built-in objects accessible in templates:
| Object | Description |
|---|---|
.Values | Values from values.yaml and overrides |
.Release.Name | The name of the release (e.g., my-mysql) |
.Release.Namespace | The namespace the release is deployed to |
.Release.Revision | The revision number (increments on upgrade) |
.Release.IsUpgrade | true if this is an upgrade operation |
.Release.IsInstall | true if this is an install operation |
.Chart.Name | The chart name from Chart.yaml |
.Chart.Version | The chart version from Chart.yaml |
.Chart.AppVersion | The appVersion from Chart.yaml |
.Capabilities.KubeVersion | The Kubernetes version of the cluster |
Control Flow
# Conditional blocks
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "mychart.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- end }}
# Looping with range
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
Template Helpers (_helpers.tpl)
The _helpers.tpl file defines reusable template snippets called with include:
# templates/_helpers.tpl
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Use include (not template) to call these helpers because include captures the output as a string that you can pipe through functions like nindent.
Values Overrides
Values can be overridden in several ways, with later sources taking precedence:
# Override with a file
helm install my-app ./mychart -f values-prod.yaml
# Override individual values
helm install my-app ./mychart --set image.tag=4.0.0
# Combine both (--set takes highest precedence)
helm install my-app ./mychart -f values-prod.yaml --set image.tag=4.0.0
# Override with multiple files (later files win)
helm install my-app ./mychart -f values-prod.yaml -f values-prod-secrets.yaml
Precedence order (lowest to highest):
- Chart's
values.yaml - Parent chart's
values.yaml(for subcharts) -f/--valuesfiles (in order)--setand--set-stringflags
Chart Dependencies
Charts can declare dependencies on other charts. Dependencies are declared in Chart.yaml:
# Chart.yaml
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: "17.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
Manage dependencies with:
# Download dependencies to charts/ directory
helm dependency update ./mychart
# List current dependencies and their status
helm dependency list ./mychart
The condition field lets users toggle dependencies on or off via values. Setting postgresql.enabled: false in values will skip installing the PostgreSQL subchart.
Helm Hooks
Hooks let you run operations at specific points in a release lifecycle. A hook is a regular Kubernetes resource (usually a Job) with a special annotation:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "mychart.fullname" . }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
Available hook types:
| Hook | When It Runs |
|---|---|
pre-install | After templates are rendered, before any resources are created |
post-install | After all resources are created |
pre-upgrade | After templates are rendered, before any resources are updated |
post-upgrade | After all resources are updated |
pre-delete | Before any resources are deleted |
post-delete | After all resources are deleted |
pre-rollback | Before a rollback is executed |
post-rollback | After a rollback is executed |
test | When helm test is invoked |
The hook-weight annotation controls execution order (lower numbers run first). The hook-delete-policy controls cleanup: hook-succeeded deletes the hook resource after it succeeds, hook-failed deletes it after failure, and before-hook-creation deletes any existing hook resource before creating a new one.
Essential Commands
# --- Repository Management ---
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/mysql --versions
# --- Install and Upgrade ---
helm install my-mysql bitnami/mysql \
--namespace databases --create-namespace \
--set auth.rootPassword=secret \
-f values-prod.yaml
helm upgrade my-mysql bitnami/mysql \
--namespace databases \
--set auth.rootPassword=new-secret \
--reuse-values # Keep previously set values
# --- Inspect and Debug ---
helm list --all-namespaces # List all releases
helm status my-mysql -n databases
helm history my-mysql -n databases
helm get values my-mysql -n databases # Show applied values
helm get manifest my-mysql -n databases # Show rendered manifests
# --- Rollback ---
helm rollback my-mysql 2 -n databases # Rollback to revision 2
# --- Uninstall ---
helm uninstall my-mysql -n databases
# --- Dry Run and Template ---
helm template my-mysql bitnami/mysql -f values-prod.yaml # Local render
helm install my-mysql bitnami/mysql --dry-run --debug # Server-side dry run
helm template vs helm install --dry-run
helm template: Renders templates locally without contacting the cluster. It cannot validate API versions or resource conflicts. Fast and useful for CI pipelines.helm install --dry-run: Sends the rendered manifests to the cluster's API server for validation without actually creating resources. More accurate but requires cluster access.
helm diff (Plugin)
The helm-diff plugin shows a diff of what would change during an upgrade:
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade my-mysql bitnami/mysql -f values-prod.yaml
This is invaluable for reviewing changes before applying them in production.
OCI Registry Support
Helm 3.8+ supports OCI (Open Container Initiative) registries as first-class chart storage. This eliminates the need for a separate chart repository server.
# Login to an OCI registry
helm registry login ghcr.io -u myuser
# Package a chart
helm package ./mychart
# Push to an OCI registry
helm push mychart-1.2.0.tgz oci://ghcr.io/myorg/charts
# Install directly from an OCI registry
helm install my-app oci://ghcr.io/myorg/charts/mychart --version 1.2.0
# Pull a chart from OCI
helm pull oci://ghcr.io/myorg/charts/mychart --version 1.2.0
OCI registries supported include Docker Hub, Amazon ECR, GitHub Container Registry (GHCR), Google Artifact Registry, and Azure Container Registry.
Creating Your Own Chart
# Scaffold a new chart
helm create my-web-app
# This generates:
# my-web-app/
# Chart.yaml
# values.yaml
# templates/
# deployment.yaml
# service.yaml
# ingress.yaml
# hpa.yaml
# serviceaccount.yaml
# _helpers.tpl
# NOTES.txt
# tests/
# test-connection.yaml
# charts/
# .helmignore
After customizing templates and values:
# Lint the chart for errors
helm lint ./my-web-app
# Package into a .tgz archive
helm package ./my-web-app
# Test the rendered output
helm template test-release ./my-web-app -f test-values.yaml
Helm vs Kustomize
Both tools solve the problem of managing Kubernetes manifests across environments, but with fundamentally different approaches:
| Aspect | Helm | Kustomize |
|---|---|---|
| Approach | Templating -- manifests are generated from templates + values | Patching -- base manifests are overlaid with modifications |
| Syntax | Go template language ({{ .Values.x }}) | Pure YAML (patches and overlays) |
| Learning Curve | Steeper -- requires learning Go templates, chart structure | Gentler -- uses standard YAML with a few conventions |
| Package Management | Full package manager with repos, versioning, dependencies | No packaging concept -- operates on directories of YAML |
| Lifecycle Management | install, upgrade, rollback, uninstall with revision history | No lifecycle management -- relies on kubectl apply |
| Ecosystem | Thousands of community charts on Artifact Hub | Built into kubectl -- no additional tooling needed |
| Best For | Distributing reusable third-party applications | Customizing your own application manifests per environment |
Many teams use both: Helm for installing third-party charts (databases, monitoring stacks) and Kustomize for managing their own application manifests.
Common Pitfalls
-
Forgetting
--reuse-valueson upgrade: By default,helm upgraderesets all values to the chart defaults. If you previously set custom values, they will be lost. Use--reuse-valuesor always pass the same-f values.yamlfile. -
Not pinning chart versions: Using
helm install my-app bitnami/mysqlwithout--versioninstalls the latest chart version, which may introduce breaking changes. Always pin versions in production. -
Whitespace issues in templates: Go templates are sensitive to whitespace. Use
{{-(trim left) and-}}(trim right) to remove unwanted blank lines. Incorrect indentation in rendered YAML is the most common template bug. -
Secrets in
values.yaml: Never commit secrets in plain text tovalues.yaml. Usehelm-secretsplugin with SOPS, or inject secrets via--setfrom CI/CD environment variables, or use External Secrets Operator. -
Not using
helm diffbefore upgrades: Blindly runninghelm upgradein production is dangerous. Always review changes withhelm diff upgradefirst. -
Ignoring NOTES.txt: The
NOTES.txttemplate is displayed after install/upgrade and is the right place to give users connection instructions, default credentials, and next steps. -
Using
helm templateas the sole validation:helm templaterenders locally and cannot catch server-side issues like invalid API versions, missing CRDs, or namespace conflicts. Use--dry-runwith a real cluster for thorough validation.
Best Practices
-
Use
values.schema.jsonto validate user-supplied values. This catches misconfigurations early with clear error messages instead of cryptic template failures. -
Version your charts with SemVer. Increment the chart
versionon every change. UseappVersionto track the application version separately. -
Use named templates in
_helpers.tplfor labels, selectors, and resource names. This ensures consistency and makes updates easy. -
Set sensible defaults in
values.yamlso the chart works out of the box withhelm install my-app ./mychartand zero overrides. -
Always include resource requests and limits as configurable values. Default to conservative values that work for development.
-
Use
helm lintin CI to catch template errors before merging chart changes. -
Store environment-specific values files (e.g.,
values-dev.yaml,values-prod.yaml) in version control alongside the chart. -
Use
helm diffin your CD pipeline as a gate before applying upgrades to production.
What's Next?
- Kustomize -- Learn the alternative overlay-based approach to managing Kubernetes manifests, and when to choose it over Helm.
- Storage (PV & PVC) -- Template StorageClasses and PVCs in Helm charts for stateful applications.
- Deployments -- Understand the Deployment resource that most Helm charts create under the hood.
- ConfigMaps & Secrets -- Learn how Helm charts typically manage application configuration.