Kubernetes Helm: The Complete Guide for 2026
Helm is the package manager for Kubernetes. Instead of managing dozens of raw YAML manifests for every application, Helm bundles them into reusable, versioned charts with configurable values. A single helm install command can deploy a complete application stack — Deployments, Services, ConfigMaps, Ingress rules, and everything in between.
This guide covers Helm from installation through production-grade workflows. You will learn chart structure, Go template syntax, repository management, hooks, dependencies, secrets handling, Helmfile for multi-release management, and CI/CD integration with GitHub Actions and ArgoCD. If you are new to Kubernetes, start with our Kubernetes Complete Guide first.
Table of Contents
- What is Helm and Why Use It
- Installing Helm
- Core Concepts: Charts, Releases, Repositories, Values
- helm install, upgrade, rollback, uninstall
- Chart Structure
- Template Syntax
- values.yaml and --set Overrides
- Helm Repositories
- Creating Your Own Charts
- Chart Dependencies and Subcharts
- Helm Hooks
- Helm Secrets and Sensitive Values
- Helmfile for Multiple Releases
- CI/CD with Helm
- Debugging Helm
- Best Practices and Common Mistakes
- Frequently Asked Questions
1. What is Helm and Why Use It
Managing Kubernetes applications means managing many YAML files — Deployments, Services, ConfigMaps, Secrets, Ingress rules, ServiceAccounts, RBAC, and more. A typical production application might require 10 to 20 separate manifests. Helm solves this by packaging everything into a single chart that can be installed, upgraded, and rolled back as a unit.
Why Helm matters:
- Package management — install complex applications (PostgreSQL, Redis, Prometheus) with a single command
- Templating — use Go templates to parameterize manifests instead of copying and editing YAML for each environment
- Release management — track every installation as a versioned release with full upgrade and rollback history
- Dependency management — declare chart dependencies (e.g., your app depends on Redis) and Helm installs them together
- Reproducibility — the same chart with the same values always produces the same manifests
- Ecosystem — thousands of community charts on Artifact Hub for databases, monitoring, ingress controllers, and more
2. Installing Helm
Helm is a single binary. You need a working kubectl with a configured kubeconfig pointing to your cluster.
# macOS
brew install helm
# Any Linux (official install script)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Verify
helm version
Helm uses your kubeconfig automatically. If kubectl get nodes works, Helm will work too. Ubuntu/Debian users can also install via apt by adding the Helm repository.
3. Core Concepts: Charts, Releases, Repositories, Values
Four concepts define how Helm works:
Chart — a package containing all the Kubernetes resource templates and metadata needed to deploy an application. Think of it as an apt package or npm module for Kubernetes.
Release — a running instance of a chart in a cluster. You can install the same chart multiple times with different release names (e.g., prod-redis and staging-redis from the same Redis chart).
Repository — a server that hosts packaged charts for download. Similar to npm registry or Docker Hub. The largest public index is Artifact Hub.
Values — configuration parameters passed to a chart to customize its behavior. Every chart has a values.yaml with defaults that you override for your deployment.
# The relationship:
# Chart (package) + Values (config) = Release (running instance)
#
# Example:
# bitnami/postgresql chart + my-values.yaml = "prod-db" release
4. helm install, upgrade, rollback, uninstall
These four commands form the core Helm workflow:
# Install a chart as a new release
helm install my-release bitnami/nginx -f custom-values.yaml
helm install my-release bitnami/nginx -n production --create-namespace
# Upgrade an existing release
helm upgrade my-release bitnami/nginx --set replicaCount=3
# Upgrade or install if not present (idempotent -- best for CI/CD)
helm upgrade --install my-release bitnami/nginx -f values.yaml
# Rollback to a previous revision
helm rollback my-release 1
# Uninstall a release (removes all K8s resources)
helm uninstall my-release
# List releases and view history
helm list --all-namespaces
helm history my-release
# Atomic upgrade: auto-rollback on failure (critical for production)
helm upgrade --install my-release ./my-chart \
-f prod-values.yaml --atomic --timeout 5m
5. Chart Structure
Every Helm chart follows a standard directory layout:
my-chart/
Chart.yaml # Chart metadata (name, version, description)
values.yaml # Default configuration values
charts/ # Directory for chart dependencies
templates/ # Kubernetes manifest templates
deployment.yaml
service.yaml
ingress.yaml
hpa.yaml
serviceaccount.yaml
configmap.yaml
_helpers.tpl # Template helper functions (partials)
NOTES.txt # Post-install instructions shown to user
.helmignore # Files to exclude from packaging
README.md # Chart documentation
Chart.yaml
apiVersion: v2 # v2 for Helm 3
name: my-app
description: A web application chart
type: application # "application" or "library"
version: 1.2.0 # Chart version (SemVer)
appVersion: "3.1.0" # Version of the app being deployed
keywords:
- web
- api
maintainers:
- name: DevTeam
email: dev@example.com
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
The version field is the chart's own version and should be incremented on every change. The appVersion is the version of the application the chart deploys — they are independent.
6. Template Syntax
Helm templates use Go's text/template package with Sprig functions added. Template expressions are wrapped in {{ }} double curly braces.
Basic Expressions and Built-in Objects
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
labels:
app: {{ .Chart.Name }}
version: {{ .Chart.AppVersion }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
Key built-in objects: .Values (from values.yaml), .Release.Name / .Release.Namespace, .Chart.Name / .Chart.Version / .Chart.AppVersion, .Template.Name, and .Capabilities.KubeVersion.
Conditionals, Loops, and Named Templates
# Conditional: only create Ingress if enabled
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
spec:
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ $.Release.Name }}-svc
port:
number: {{ .port }}
{{- end }}
{{- end }}
{{- end }}
# Named template in _helpers.tpl
{{- define "my-chart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
# Usage: {{ include "my-chart.fullname" . }}
Useful Sprig Functions
{{ .Values.name | upper }} # Uppercase
{{ .Values.name | quote }} # Wrap in quotes
{{ .Values.data | toYaml | nindent 4 }} # Convert to YAML, indent
{{ default "nginx" .Values.image }} # Default value
{{ required "image is required" .Values.image }} # Fail if missing
{{ .Values.password | b64enc }} # Base64 encode
7. values.yaml and --set Overrides
The values.yaml file defines every configurable parameter with sensible defaults:
# values.yaml
replicaCount: 2
image:
repository: myapp
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
annotations: {}
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
port: 80
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 80
postgresql:
enabled: true
auth:
database: myapp
Overriding Values
# Override with files (-f can be used multiple times, later wins)
helm install my-release ./my-chart -f base.yaml -f prod.yaml
# Override individual values with --set
helm install my-release ./my-chart --set replicaCount=5,image.tag=v2.1.0
# View the final merged values for a release
helm get values my-release --all
Precedence order (last wins): chart defaults → parent chart values → first -f file → next -f file → --set flags. Use --set-string when a numeric value should remain a string.
8. Helm Repositories
Helm repositories host packaged charts. You add them locally, then install charts from them.
# Add popular repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
# Update local cache (always do this before installing)
helm repo update
# Search for charts locally and on Artifact Hub
helm search repo nginx
helm search hub prometheus
# Show chart info and default values
helm show chart bitnami/nginx
helm show values bitnami/nginx
OCI Registries
Helm 3 also supports OCI registries, using the same infrastructure as container images:
# Push and install from OCI registry
helm package my-chart/
helm push my-chart-1.0.0.tgz oci://ghcr.io/myorg/charts
helm install my-release oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
9. Creating Your Own Charts
The helm create command scaffolds a complete chart with best-practice templates:
# Scaffold a new chart (creates Chart.yaml, values.yaml, templates/)
helm create my-app
# Customize: edit Chart.yaml, values.yaml, and templates/
# Test rendering, lint, and package
helm template my-release ./my-app
helm lint ./my-app
helm package ./my-app # Creates my-app-1.0.0.tgz
helm install my-release ./my-app -f my-values.yaml
The scaffolded chart includes deployment.yaml, service.yaml, ingress.yaml, hpa.yaml, serviceaccount.yaml, _helpers.tpl, NOTES.txt (shown after install), and a test directory. Customize the templates for your application and add extra resources (CronJobs, PVCs) as needed.
10. Chart Dependencies and Subcharts
Charts can depend on other charts. Declare dependencies 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
- name: common
version: "2.x.x"
repository: "https://charts.bitnami.com/bitnami"
tags:
- bitnami-common
# Download dependencies into charts/ directory
helm dependency update ./my-app
helm dependency build ./my-app
# List dependencies and their status
helm dependency list ./my-app
Configure subchart values by nesting them under the dependency name in your values.yaml:
# values.yaml
postgresql:
enabled: true
auth:
postgresPassword: "secret"
database: "myapp"
primary:
resources:
requests:
cpu: 250m
memory: 256Mi
redis:
enabled: true
architecture: standalone
auth:
enabled: false
Use condition to make dependencies optional. When postgresql.enabled is false, the PostgreSQL subchart is not rendered at all.
11. Helm Hooks
Hooks let you run Kubernetes resources at specific points in the release lifecycle. Annotate any resource with helm.sh/hook to make it a hook:
# templates/db-migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation
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: {{ .Release.Name }}-db-secret
key: url
Available Hook Events
pre-install— runs before any release resources are createdpost-install— runs after all resources are createdpre-upgrade— runs before an upgradepost-upgrade— runs after an upgradepre-rollback/post-rollback— before and after rollbackpre-delete/post-delete— before and after uninstalltest— runs whenhelm testis invoked
Hook delete policies control cleanup: before-hook-creation deletes the previous hook resource before creating a new one, hook-succeeded deletes after success, hook-failed deletes after failure. Use before-hook-creation for most Jobs to avoid "already exists" errors on repeated upgrades.
12. Helm Secrets and Sensitive Values
Never commit plaintext secrets to Git. Helm has several strategies for handling sensitive values securely.
helm-secrets Plugin (SOPS Integration)
# Install the plugin
helm plugin install https://github.com/jkroepke/helm-secrets
# Create a SOPS config (.sops.yaml)
creation_rules:
- encrypted_regex: "^(password|secret|token|key)$"
kms: "arn:aws:kms:us-east-1:123456789:key/abc-123"
# Encrypt a values file
helm secrets encrypt secrets.yaml > secrets.enc.yaml
# Use encrypted values in install/upgrade
helm secrets install my-release ./my-chart -f secrets.enc.yaml
helm secrets upgrade my-release ./my-chart -f secrets.enc.yaml
# Edit encrypted values in-place
helm secrets edit secrets.enc.yaml
External Secrets Operator
Pull secrets from external providers (AWS Secrets Manager, Vault, GCP Secret Manager) at runtime:
# templates/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ .Release.Name }}-db-creds
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: {{ .Release.Name }}-db-secret
data:
- secretKey: password
remoteRef:
key: prod/myapp/db
property: password
CI/CD Injection
# Pass secrets as --set from CI environment variables
helm upgrade --install my-release ./my-chart \
--set database.password="${DB_PASSWORD}" \
--set api.secretKey="${API_SECRET}"
13. Helmfile for Managing Multiple Releases
When you manage many Helm releases across environments, Helmfile provides a declarative way to define, install, and sync them all:
# helmfile.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
environments:
dev:
values:
- environments/dev.yaml
staging:
values:
- environments/staging.yaml
production:
values:
- environments/production.yaml
releases:
- name: my-app
namespace: app
chart: ./charts/my-app
values:
- values/{{ .Environment.Name }}/app.yaml
set:
- name: image.tag
value: {{ requiredEnv "IMAGE_TAG" }}
- name: postgresql
namespace: database
chart: bitnami/postgresql
version: 12.5.0
values:
- values/{{ .Environment.Name }}/postgres.yaml
condition: postgresql.enabled
- name: monitoring
namespace: monitoring
chart: prometheus-community/kube-prometheus-stack
version: 48.0.0
values:
- values/{{ .Environment.Name }}/monitoring.yaml
# Apply all releases for an environment
helmfile -e production sync
# Preview changes before applying
helmfile -e production diff
# Apply only specific releases
helmfile -e production -l name=my-app sync
# Destroy all releases
helmfile -e staging destroy
# Lint all charts
helmfile -e production lint
Helmfile is especially powerful with GitOps: commit the helmfile.yaml and environment-specific values, and let your CI pipeline run helmfile sync on merge.
14. CI/CD with Helm
GitHub Actions
# .github/workflows/deploy.yaml
name: Deploy with Helm
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: 'v3.14.0'
- name: Deploy
run: |
helm upgrade --install my-app ./charts/my-app \
-n production --create-namespace \
-f values/production.yaml \
--set image.tag=${{ github.sha }} \
--atomic \
--timeout 10m
See our GitHub Actions CI/CD guide for complete pipeline patterns including testing, building, and multi-environment deployments.
ArgoCD Integration
ArgoCD natively supports Helm charts as an application source. It watches your Git repository and automatically syncs Helm releases when values change:
# ArgoCD Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-config
targetRevision: main
path: charts/my-app
helm:
valueFiles:
- values/production.yaml
parameters:
- name: image.tag
value: "v2.1.0"
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
ArgoCD handles diff detection, sync, health checks, and rollback — replacing manual helm upgrade commands with a GitOps workflow.
15. Debugging Helm
When things go wrong, Helm provides several tools to diagnose issues:
# Render templates locally (most useful debugging command)
helm template my-release ./my-chart -f values.yaml
helm template my-release ./my-chart -s templates/deployment.yaml
# Dry run with debug output (validates against cluster API)
helm install my-release ./my-chart --dry-run --debug
# Lint a chart for issues
helm lint ./my-chart -f prod-values.yaml
# Inspect a deployed release
helm get manifest my-release # rendered K8s manifests
helm get values my-release --all # all values including defaults
helm history my-release # revision history
helm status my-release # current status
Debugging workflow: (1) helm template to verify rendering, (2) helm lint for structural problems, (3) --dry-run --debug to validate against the API server, (4) helm history + kubectl get events if a release is stuck, (5) helm rollback + kubectl logs for failed upgrades.
16. Best Practices and Common Mistakes
Best Practices
- Always use
--atomicin production upgrades to auto-rollback on failure - Pin chart versions in your dependencies and Helmfile — never use unversioned chart references in CI/CD
- Use
helm upgrade --installinstead of separate install/upgrade logic for idempotent pipelines - Store values files in Git, one per environment (dev, staging, prod) — not
--setflags scattered across scripts - Use
requiredin templates for values that must be provided:{{ required "image.tag is required" .Values.image.tag }} - Follow Kubernetes label conventions:
app.kubernetes.io/name,app.kubernetes.io/instance,app.kubernetes.io/version - Set resource requests and limits in your default values.yaml — never deploy without them
- Use
helm diffplugin to preview changes before applying:helm diff upgrade my-release ./my-chart - Version your charts using SemVer. Bump
versionon every change,appVersionwhen the deployed app changes - Use JSON Schema validation — add a
values.schema.jsonto catch misconfiguration early
Common Mistakes
- Storing secrets in values.yaml and committing to Git — use helm-secrets, External Secrets Operator, or CI/CD injection instead
- Not using
--atomic— a failed upgrade leaves the release in a brokenFAILEDstate that requires manual intervention - Forgetting
helm repo updatebefore install — you may install an outdated chart version cached locally - Using
helm installin CI/CD instead ofhelm upgrade --install— the pipeline fails on the second run - Not setting
hook-delete-policyon hook Jobs — they accumulate and the next upgrade fails with "already exists" - Ignoring NOTES.txt — it is the first thing users see after install and should contain access instructions
- Over-templating — not everything needs to be a value. Template only what genuinely changes between environments
- Skipping
helm lintandhelm templatein CI — catch errors before they reach the cluster
Frequently Asked Questions
What is the difference between Helm 2 and Helm 3?
Helm 3 removed Tiller, the server-side component that Helm 2 required inside the cluster. This eliminated a major security risk because Tiller ran with cluster-admin privileges. Helm 3 uses the kubeconfig credentials of the user running the command, supports three-way strategic merge patches for upgrades, stores release information as Kubernetes Secrets in the release namespace instead of ConfigMaps in the Tiller namespace, and uses JSON Schema validation for chart values. All new projects should use Helm 3.
How do I pass custom values to a Helm chart?
You can pass custom values in three ways. First, create a custom values file (e.g., my-values.yaml) and pass it with the -f flag: helm install my-release chart -f my-values.yaml. Second, use the --set flag for individual values: helm install my-release chart --set replicas=3,image.tag=v2.0. Third, combine both approaches where --set values override file values. You can pass multiple -f files, and values from later files override earlier ones.
What is the difference between helm install and helm upgrade --install?
helm install creates a new release and fails if a release with the same name already exists. helm upgrade --install either upgrades an existing release or installs it if it does not exist. The upgrade --install pattern is preferred in CI/CD pipelines because it is idempotent: you can run it repeatedly without worrying about whether the release already exists. Combine it with --atomic to automatically roll back if the upgrade fails.
How do I manage secrets with Helm securely?
Never store secrets in plain text in values.yaml or commit them to version control. Use the helm-secrets plugin, which integrates with Mozilla SOPS to encrypt values files using AWS KMS, GCP KMS, Azure Key Vault, or PGP keys. Alternatively, use External Secrets Operator to pull secrets from cloud secret managers into Kubernetes Secrets at runtime. For CI/CD, inject secrets as environment variables and pass them with --set, or use sealed-secrets to encrypt them in Git.
When should I use Helmfile instead of plain Helm?
Use Helmfile when you manage multiple Helm releases across multiple environments. Plain Helm works well for a single chart or a few releases, but when you have 10 or more releases with different values per environment (dev, staging, prod), Helmfile provides a declarative helmfile.yaml that defines all releases, their values, and their dependencies in one place. You can run helmfile sync to apply everything at once, helmfile diff to preview changes, and use environment-specific values files. It is especially valuable for GitOps workflows.
Conclusion
Helm transforms Kubernetes from a system where you manage sprawling YAML files into one where you install, upgrade, and roll back applications with single commands. Start with community charts from Artifact Hub for common infrastructure (databases, monitoring, ingress controllers), then create your own charts for application code. Use helm template and helm lint to catch errors early, --atomic to protect production, and Helmfile when your release count grows beyond what manual helm upgrade commands can manage.
For the full Kubernetes foundation, see our Kubernetes Complete Guide. To understand Pod-level configuration in depth, read the Kubernetes Pods Guide. And for GitOps-driven Helm deployments, explore our ArgoCD Guide.
Learn More
- Kubernetes Complete Guide — Pods, Deployments, Services, kubectl, and production patterns
- Kubernetes Pods Guide — Pod spec, lifecycle, probes, init containers, and security context
- ArgoCD Complete Guide — GitOps continuous delivery for Kubernetes
- GitHub Actions CI/CD Guide — automated pipelines for build, test, and deploy
- Docker Compose Guide — single-server container orchestration for local and simple deployments