Skip to main content

Helm: The Package Manager

Key Takeaways for AI & Readers
  • 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

Helm Engine
⚓️

deployment.yaml


apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - image: my-app:1.2.0
Generated

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:

ObjectDescription
.ValuesValues from values.yaml and overrides
.Release.NameThe name of the release (e.g., my-mysql)
.Release.NamespaceThe namespace the release is deployed to
.Release.RevisionThe revision number (increments on upgrade)
.Release.IsUpgradetrue if this is an upgrade operation
.Release.IsInstalltrue if this is an install operation
.Chart.NameThe chart name from Chart.yaml
.Chart.VersionThe chart version from Chart.yaml
.Chart.AppVersionThe appVersion from Chart.yaml
.Capabilities.KubeVersionThe 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):

  1. Chart's values.yaml
  2. Parent chart's values.yaml (for subcharts)
  3. -f / --values files (in order)
  4. --set and --set-string flags

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:

HookWhen It Runs
pre-installAfter templates are rendered, before any resources are created
post-installAfter all resources are created
pre-upgradeAfter templates are rendered, before any resources are updated
post-upgradeAfter all resources are updated
pre-deleteBefore any resources are deleted
post-deleteAfter all resources are deleted
pre-rollbackBefore a rollback is executed
post-rollbackAfter a rollback is executed
testWhen 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:

AspectHelmKustomize
ApproachTemplating -- manifests are generated from templates + valuesPatching -- base manifests are overlaid with modifications
SyntaxGo template language ({{ .Values.x }})Pure YAML (patches and overlays)
Learning CurveSteeper -- requires learning Go templates, chart structureGentler -- uses standard YAML with a few conventions
Package ManagementFull package manager with repos, versioning, dependenciesNo packaging concept -- operates on directories of YAML
Lifecycle Managementinstall, upgrade, rollback, uninstall with revision historyNo lifecycle management -- relies on kubectl apply
EcosystemThousands of community charts on Artifact HubBuilt into kubectl -- no additional tooling needed
Best ForDistributing reusable third-party applicationsCustomizing 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

  1. Forgetting --reuse-values on upgrade: By default, helm upgrade resets all values to the chart defaults. If you previously set custom values, they will be lost. Use --reuse-values or always pass the same -f values.yaml file.

  2. Not pinning chart versions: Using helm install my-app bitnami/mysql without --version installs the latest chart version, which may introduce breaking changes. Always pin versions in production.

  3. 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.

  4. Secrets in values.yaml: Never commit secrets in plain text to values.yaml. Use helm-secrets plugin with SOPS, or inject secrets via --set from CI/CD environment variables, or use External Secrets Operator.

  5. Not using helm diff before upgrades: Blindly running helm upgrade in production is dangerous. Always review changes with helm diff upgrade first.

  6. Ignoring NOTES.txt: The NOTES.txt template is displayed after install/upgrade and is the right place to give users connection instructions, default credentials, and next steps.

  7. Using helm template as the sole validation: helm template renders locally and cannot catch server-side issues like invalid API versions, missing CRDs, or namespace conflicts. Use --dry-run with a real cluster for thorough validation.


Best Practices

  1. Use values.schema.json to validate user-supplied values. This catches misconfigurations early with clear error messages instead of cryptic template failures.

  2. Version your charts with SemVer. Increment the chart version on every change. Use appVersion to track the application version separately.

  3. Use named templates in _helpers.tpl for labels, selectors, and resource names. This ensures consistency and makes updates easy.

  4. Set sensible defaults in values.yaml so the chart works out of the box with helm install my-app ./mychart and zero overrides.

  5. Always include resource requests and limits as configurable values. Default to conservative values that work for development.

  6. Use helm lint in CI to catch template errors before merging chart changes.

  7. Store environment-specific values files (e.g., values-dev.yaml, values-prod.yaml) in version control alongside the chart.

  8. Use helm diff in 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.