Kubernetes Helm: The Complete Guide for 2026

Published February 12, 2026 · 30 min read

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.

⚙ Related: Generate Kubernetes manifests with the Kubernetes YAML Generator and validate your YAML with the YAML Validator.

Table of Contents

  1. What is Helm and Why Use It
  2. Installing Helm
  3. Core Concepts: Charts, Releases, Repositories, Values
  4. helm install, upgrade, rollback, uninstall
  5. Chart Structure
  6. Template Syntax
  7. values.yaml and --set Overrides
  8. Helm Repositories
  9. Creating Your Own Charts
  10. Chart Dependencies and Subcharts
  11. Helm Hooks
  12. Helm Secrets and Sensitive Values
  13. Helmfile for Multiple Releases
  14. CI/CD with Helm
  15. Debugging Helm
  16. Best Practices and Common Mistakes
  17. 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:

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

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

Common Mistakes

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.

⚙ Related: Automate Helm deployments with GitHub Actions CI/CD, manage GitOps with ArgoCD, and provision clusters with Terraform.

Learn More

Related Resources

Kubernetes Complete Guide
Pods, Deployments, Services, and production patterns
Kubernetes Pods Guide
Pod spec, lifecycle, probes, and security context
ArgoCD Complete Guide
GitOps continuous delivery for Kubernetes
GitHub Actions CI/CD Guide
Automated pipelines for build, test, and deploy