Kubernetes Helm: Package Manager for Kubernetes (2026)

Helm is the package manager for Kubernetes — it bundles related Kubernetes manifests into versioned charts, manages dependencies between charts, handles upgrades and rollbacks, and lets you customise deployments through values files. If you're deploying any non-trivial application to Kubernetes, Helm saves enormous time.

1. Core Concepts

  • Chart: A package of Kubernetes manifests with templates and default values
  • Release: An installed instance of a chart. You can install the same chart multiple times with different release names
  • Repository: A place to store and share charts (like npm registry for Kubernetes)
  • Values: Configuration that customises a chart's templates
# Install Helm 3
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Verify
helm version

2. Installing Charts

# Add a repo
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add cert-manager https://charts.jetstack.io
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Search for charts
helm search repo nginx
helm search hub wordpress  # searches Artifact Hub

# Install with default values
helm install my-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace

# Install with custom values
helm install my-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  --set controller.replicaCount=2 \
  --set controller.service.type=LoadBalancer

# Install from a values file
helm install my-nginx ingress-nginx/ingress-nginx \
  -f my-nginx-values.yaml \
  --namespace ingress-nginx --create-namespace

# Dry run (preview manifests without installing)
helm install my-nginx ingress-nginx/ingress-nginx --dry-run --debug

3. Chart Structure

helm create myapp  # scaffolds a new chart

# Generated structure:
myapp/
├── Chart.yaml          # Chart metadata (name, version, dependencies)
├── values.yaml         # Default configuration values
├── charts/             # Chart dependencies (subcharts)
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   ├── _helpers.tpl    # Reusable template functions
│   └── NOTES.txt       # Post-install instructions
└── .helmignore

Chart.yaml with dependencies:

apiVersion: v2
name: myapp
description: My application Helm chart
type: application
version: 1.2.0        # Chart version
appVersion: "2.4.1"   # App version (for display)

dependencies:
  - name: postgresql
    version: "13.2.0"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled
  - name: redis
    version: "18.0.0"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled
# Download dependencies
helm dependency update myapp/

4. Template Syntax

# templates/deployment.yaml
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:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        ports:
        - containerPort: {{ .Values.service.port }}
        {{- if .Values.resources }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
        {{- end }}
        env:
        {{- range .Values.env }}
        - name: {{ .name }}
          value: {{ .value | quote }}
        {{- end }}

Reusable helpers in _helpers.tpl:

{{/* Generate the full name */}}
{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/* Common labels */}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
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 }}

5. Values and Overrides

# values.yaml — defaults
replicaCount: 1
image:
  repository: myregistry/myapp
  tag: ""  # defaults to Chart.AppVersion
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 8080
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    memory: 512Mi
env: []
postgresql:
  enabled: true
  auth:
    database: myapp
# Override for production
helm install myapp ./myapp \
  -f values.yaml \
  -f values-production.yaml \  # production overrides
  --set image.tag=1.2.3 \
  --set replicaCount=3

# Show computed values
helm get values myapp-release
helm get manifest myapp-release  # see rendered manifests

6. Upgrade, Rollback and Uninstall

# Upgrade — applies changes and bumps revision
helm upgrade myapp-release ./myapp \
  --set image.tag=1.3.0 \
  --atomic \          # rolls back automatically if upgrade fails
  --timeout 5m \
  --wait              # wait for all pods to be ready

# View release history
helm history myapp-release

# Rollback to previous revision
helm rollback myapp-release 2  # rollback to revision 2
helm rollback myapp-release 0  # rollback to previous

# Uninstall (removes all K8s resources, keeps history by default)
helm uninstall myapp-release
helm uninstall myapp-release --keep-history  # preserve history for rollback

7. Helm Hooks

Hooks run Jobs at specific points in the release lifecycle:

# templates/db-migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-db-migrate
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        command: ["python", "manage.py", "migrate"]
      restartPolicy: Never

8. Helmfile

Helmfile declaratively manages multiple Helm releases across environments:

# helmfile.yaml
environments:
  staging:
    values: ["envs/staging.yaml"]
  production:
    values: ["envs/production.yaml"]

releases:
  - name: ingress-nginx
    namespace: ingress-nginx
    chart: ingress-nginx/ingress-nginx
    version: 4.9.1
    values:
      - controller.replicaCount: 2

  - name: cert-manager
    namespace: cert-manager
    chart: jetstack/cert-manager
    version: 1.14.0
    set:
      - name: installCRDs
        value: true

  - name: myapp
    namespace: production
    chart: ./charts/myapp
    values:
      - values/myapp-{{ .Environment.Name }}.yaml
helmfile sync --environment production   # apply all releases
helmfile diff --environment staging       # preview changes
helmfile destroy                          # remove all releases

Frequently Asked Questions

What is the difference between helm install and helm upgrade --install?

helm install fails if the release already exists. helm upgrade --install installs if it doesn't exist, upgrades if it does — perfect for CI/CD pipelines where you want idempotent deploys.

How do I debug template rendering errors?

Use helm template ./myapp to render templates locally without installing. Use --debug for verbose output. Use helm lint ./myapp to catch common mistakes before deploying.

How do I handle secrets in Helm?

Don't store plaintext secrets in values.yaml. Options: use Helm Secrets plugin (encrypts with SOPS/age), reference existing Kubernetes Secrets with valueFrom.secretKeyRef in templates, use External Secrets Operator to sync from AWS Secrets Manager, or inject via CI/CD pipeline with --set secret.value=.

What is the difference between Helm 2 and Helm 3?

Helm 3 (current) removed Tiller (the server-side component) — it uses your local kubeconfig credentials directly. This eliminates RBAC issues and makes Helm cluster-admin no longer required. Helm 2 is EOL and should not be used.