Helm Charts — Complete Kubernetes Package Manager Guide 2026

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Deploying applications to Kubernetes involves dozens of YAML files — Deployments, Services, ConfigMaps, Ingress rules, and more. Managing these files individually across multiple environments (dev, staging, production) leads to configuration drift, duplication, and manual errors.

Helm solves this by treating your Kubernetes manifests as templates that accept variables. A single Helm chart can deploy identically structured applications across any environment by changing only a values file. In 2026, Helm is the de-facto standard for Kubernetes application packaging, used by projects from NGINX Ingress to cert-manager to Prometheus.

What Is a Helm Chart

A Helm chart is a directory structure containing:

mychart/
  Chart.yaml          # Chart metadata
  values.yaml         # Default configuration values
  templates/          # Kubernetes manifest templates
    deployment.yaml
    service.yaml
    ingress.yaml
    _helpers.tpl      # Reusable template snippets
  charts/             # Dependency sub-charts

Chart.yaml defines the chart identity:

apiVersion: v2
name: mychart
description: A Helm chart for my application
type: application
version: 0.1.0
appVersion: "1.0.0"

Helm Templating Syntax

Helm uses Go's text/template engine with Sprig helper functions. Templates interpolate values using double curly braces.

Basic deployment template:

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 | default .Chart.AppVersion }}"
          ports:
            - containerPort: {{ .Values.service.port }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Corresponding values.yaml:

replicaCount: 2
 
image:
  repository: nginx
  tag: ""
 
service:
  type: ClusterIP
  port: 80
 
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi
 
ingress:
  enabled: false
  host: ""

Installing and Managing Releases

Install a chart:

# Install from a local directory
helm install myapp ./mychart
 
# Install with custom values file
helm install myapp ./mychart -f values.production.yaml
 
# Install overriding specific values inline
helm install myapp ./mychart \
  --set image.tag=v2.1.0 \
  --set replicaCount=3
 
# Install into a specific namespace
helm install myapp ./mychart -n production --create-namespace

Upgrade and rollback:

# Upgrade a release
helm upgrade myapp ./mychart -f values.production.yaml
 
# Atomic upgrade — auto-rollback on failure
helm upgrade myapp ./mychart --atomic --timeout 5m
 
# View release history
helm history myapp
 
# Rollback to a specific revision
helm rollback myapp 1

Inspect and debug:

helm list -A                    # All releases across namespaces
helm get values myapp           # See computed values
helm get manifest myapp         # See rendered manifests
helm template myapp ./mychart   # Render locally without installing
helm status myapp               # Release status summary

Working With Chart Repositories

# Add popular repos
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add cert-manager https://charts.jetstack.io
 
# Update repo cache
helm repo update
 
# Search for a chart
helm search repo nginx
 
# Inspect default values before installing
helm show values ingress-nginx/ingress-nginx
 
# Install NGINX Ingress Controller
helm install ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer

Helm Hooks and Tests

Hooks let you run Jobs at specific points in the release lifecycle — ideal for database migrations.

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

Helm test to verify a deployment:

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "mychart.fullname" . }}-test"
  annotations:
    "helm.sh/hook": test
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}']

Run with helm test myapp after installation.

Packaging and Publishing Charts

# Lint a chart for syntax and logic errors
helm lint ./mychart
 
# Package into a versioned .tgz archive
helm package ./mychart
 
# Push to an OCI registry (Helm 3.8+)
helm push mychart-0.1.0.tgz oci://ghcr.io/myorg/charts
 
# Install from OCI registry
helm install myapp oci://ghcr.io/myorg/charts/mychart --version 0.1.0

Common Mistakes

  • Hardcoding image tags — use {{ .Values.image.tag | default .Chart.AppVersion }} so upgrades remain declarative.
  • Skipping helm lint — run it in CI before merging chart changes; it catches template syntax errors early.
  • Not using --atomic — without it, a failed upgrade leaves a broken release; --atomic triggers automatic rollback.
  • Ignoring hook weights — when multiple hooks fire at the same lifecycle point, undefined order causes race conditions.
  • Storing secrets in values.yaml — use Kubernetes Secrets, external-secrets operator, or the Helm Secrets plugin instead.
  • Forgetting helm repo update — stale indexes cause "chart not found" errors during installs.

Best Practices

  • One chart per application — avoid mega-charts; model shared components (Redis, PostgreSQL) as sub-chart dependencies.
  • Version your charts semantically — bump version in Chart.yaml on every chart change; appVersion tracks the container image version.
  • Use named templates in _helpers.tpl — keep labels, selectorLabels, and fullname helpers DRY across all templates.
  • Environment-specific values files — maintain values.dev.yaml, values.staging.yaml, values.production.yaml; never mutate the base values.yaml per environment.
  • Preview with helm template — render manifests locally before helm install to catch issues without touching the cluster.
  • Adopt OCI registries — push charts to GitHub Container Registry or AWS ECR; OCI support is stable in Helm 3.8+.

Key Takeaways

  • Helm is the Kubernetes package manager that templatizes YAML manifests and manages versioned releases across environments.
  • A chart is a versioned directory containing Chart.yaml, values.yaml, and a templates/ folder with Go template files.
  • helm install, helm upgrade, and helm rollback manage the full lifecycle of a Helm release.
  • Helm hooks run Kubernetes Jobs at specific lifecycle points (pre-install, pre-upgrade, post-upgrade) — ideal for database migrations.
  • The --atomic flag on helm upgrade automatically rolls back to the previous working release if the upgrade fails.
  • Chart repositories can be traditional HTTP indexes or modern OCI registries; OCI support has been stable since Helm 3.8.
  • Secrets must never be stored in values.yaml; use Kubernetes Secrets, Vault, or the Helm Secrets plugin with encryption.
  • Always run helm lint and helm template in CI pipelines before deploying chart changes to any environment.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading