Verified16 commandsAI-assisted

Helm

.md

Verified against Helm v3.18.6, flags verified via `helm <cmd> --help` / `helm get --help` run locally, 2026-08-29 · official docs

What it is and where it fits 🎯#

Helm is Kubernetes' package manager — a "chart" bundles a set of templated manifests plus a schema of configurable values, so installing a complex multi-resource application (a database, a message queue, an entire observability stack) becomes one command with a values file, instead of hand-assembling dozens of raw YAML files per environment. Helm 3 (the current major version) dropped Helm 2's server-side Tiller component entirely — every Helm operation today talks directly to the Kubernetes API using your own kubeconfig credentials, which is also why helm's RBAC exposure is exactly whatever your kubeconfig's identity already has, nothing more.

The chart → release lifecycle#

Diagram

A Release is a specific, named, versioned instance of a chart installed into a cluster — the same chart installed twice under different release names (e.g. postgresql-app1, postgresql-app2) are two completely independent releases with independent histories, values, and lifecycles.

Installation#

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash   # official install script
brew install helm

helm version

Managing chart repositories#

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update                          # refresh the local index of all added repos
helm repo list
helm search repo postgresql               # search across all added repos

Installing a release#

helm install my-release bitnami/postgresql --namespace my-app --create-namespace
helm install my-release ./my-chart -f values-prod.yaml
helm install my-release ./my-chart --set image.tag=v2.1.0,replicaCount=3
helm install my-release ./my-chart --version 1.4.2   # pin a specific chart version

Tip

-f values.yaml and --set key=value can both be used together — --set values win when the same key is set both ways. This makes --set the standard way to override one or two values from a CI pipeline (an image tag, a replica count computed at deploy time) without maintaining a separate values file per environment just for those couple of dynamic fields.

Upgrading a release#

helm upgrade my-release ./my-chart -f values-prod.yaml
helm upgrade --install my-release ./my-chart -f values-prod.yaml   # upgrade if it exists, install if it doesn't
helm upgrade my-release ./my-chart --atomic                          # auto-rollback the whole upgrade on failure
helm upgrade my-release ./my-chart --atomic --timeout 5m               # bound how long --atomic waits before giving up and rolling back

Tip

--install on upgrade (upgrade -i) is the standard pattern for idempotent CI/CD deploy scripts — one command works whether this is the first deploy or the hundredth, with no separate install-vs-upgrade branching logic needed in the pipeline.

Important

--atomic is what actually makes a failed upgrade safe. Without it, a rollout that fails partway through leaves the release in a broken, half-upgraded state that needs a manual helm rollback. With --atomic, Helm automatically rolls back to the last successful revision the moment it detects the upgrade failed (a pod never became Ready within --timeout, a hook failed, etc.) — this should be the default for any production deploy pipeline, not an opt-in.

Previewing changes before applying#

helm install my-release ./my-chart --dry-run --debug          # render manifests locally, no cluster call
helm template my-release ./my-chart -f values-prod.yaml        # render manifests to stdout, no release created at all
helm diff upgrade my-release ./my-chart -f values-prod.yaml    # requires the helm-diff plugin — shows an actual diff

helm diff is a plugin, not built into core Helm — install it with helm plugin install https://github.com/databus23/helm-diff. It's the closest thing Helm has to terraform plan: an actual before/after diff against the live cluster state, versus template/--dry-run which only show the rendered output in isolation, with no comparison to what's actually running.

Rolling back#

helm history my-release                    # list all revisions of a release
helm rollback my-release                   # roll back to the previous revision
helm rollback my-release 3                 # roll back to a specific revision number
helm rollback my-release 3 --dry-run        # preview what a rollback would change before actually doing it

Listing and removing releases#

helm list --namespace my-app
helm list --all-namespaces
helm status my-release
helm uninstall my-release --namespace my-app
helm uninstall my-release --keep-history      # uninstall but keep the release record (allows a later `helm rollback` to "undelete")

Chart development helpers#

helm lint ./my-chart                         # catch template/schema issues before installing
helm create my-new-chart                     # scaffold a new chart from the standard starter template
helm package ./my-chart                       # produce a .tgz for distribution
helm package ./my-chart --sign --key 'my-key' --keyring ~/.gnupg/secring.gpg   # sign the package for supply-chain integrity

Debugging rendered templates 🔍#

helm template my-release ./my-chart -s templates/deployment.yaml   # render just one template
helm template my-release ./my-chart --debug                        # show the computed values alongside the output
helm template my-release ./my-chart --validate                     # validate rendered manifests against the live cluster's API
helm template my-release ./my-chart --set replicaCount=3 --set-string image.tag=1.2.3

-s/--show-only (repeatable) is the fastest way to check one resource in a chart with dozens of templates without scrolling past everything else. --validate actually contacts the cluster to check the rendered manifests against its API — plain template never does, which is why it can render manifests referencing a CRD that doesn't exist in that cluster without complaint.

Inspecting a live release#

helm get values my-release                  # the values actually used for the current revision (merged, not just your -f file)
helm get manifest my-release                 # the exact rendered manifests currently applied
helm get notes my-release                     # the chart's post-install NOTES.txt output, re-displayed
helm get all my-release                        # everything above, plus hooks and metadata, in one call

Tip

helm get values my-release shows the actual merged values Helm used — not just what you passed with -f/--set. This is the fastest way to answer "wait, what value is this release actually running with" months after the original deploy, when nobody remembers the exact flags used.

Managing chart dependencies#

cat my-chart/Chart.yaml                       # dependencies are declared here: name, version, repository
helm dependency list ./my-chart                # show declared deps vs what's actually in charts/
helm dependency update ./my-chart              # pull deps declared in Chart.yaml into charts/, writes Chart.lock
helm dependency build ./my-chart               # rebuild charts/ from the existing Chart.lock (no re-resolution)

Note

update re-resolves version ranges against the repo index and can pick up a newer chart than last time; build reproduces exactly what's pinned in Chart.lock. Use build in CI for reproducible installs, update when you deliberately want to bump a dependency — the same package-lock.json-vs-npm install distinction shows up here.

Testing a release 🧪#

helm test my-release                          # run the test hooks defined in the chart (helm.sh/hook: test)
helm test my-release --logs                   # also dump logs from the test pods after they complete
helm test my-release --filter name=connection-test   # run only a specific named test

Test hooks are just Pods annotated "helm.sh/hook": test in the chart's templates — helm test finds and runs them against a release that's already installed, then reports pass/fail per pod. It's a post-install smoke test, not a substitute for helm lint/--dry-run at deploy time.

Linting a chart#

helm lint ./my-chart --strict                 # treat warnings as failures (use in CI)
helm lint ./my-chart --with-subcharts         # also lint every dependency chart under charts/
helm lint ./my-chart -f values-prod.yaml      # lint against a specific values file instead of the defaults

Note

Plain helm lint only emits [WARNING] for style/convention issues and exits 0 — --strict is what actually fails a CI pipeline on those warnings, so add it once a chart's own conventions are settled and you actually want them enforced, not just suggested.

Searching and removing repositories#

helm search repo postgresql --versions        # every version of postgresql across added repos
helm search hub ingress                       # search Artifact Hub itself, not just your added repos
helm repo remove bitnami                      # drop a repo you added earlier

search repo only searches repos you've already helm repo added locally; search hub queries Artifact Hub (artifacthub.io) directly, which is useful for discovering a chart before you know which repo it lives in.

Real-world scenario: safe production upgrade with automatic rollback#

A production upgrade should never leave a namespace half-broken if it fails partway through:

helm diff upgrade my-release ./my-chart -f values-prod.yaml     # review exactly what will change first
helm upgrade my-release ./my-chart -f values-prod.yaml \
  --atomic --timeout 5m --wait

--wait (implied by --atomic, but worth knowing explicitly) blocks until every resource in the release reports Ready, not just until the API server accepts the manifests — combined with --atomic, a failure anywhere in that wait window triggers an automatic rollback rather than leaving the cluster in an ambiguous half-upgraded state for someone to discover later.

Real-world scenario: GitOps-style CI recipe#

# .github/workflows/helm-deploy.yml
name: Helm Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/setup-helm@v4
      - name: Lint
        run: helm lint ./chart --strict
      - name: Deploy
        run: |
          helm upgrade --install my-release ./chart \
            -f values-prod.yaml \
            --namespace production --create-namespace \
            --atomic --timeout 5m

Common pitfalls#

  • Skipping --atomic on a production upgrade — see the IMPORTANT callout above; this is the single highest-value flag on this whole page for production safety.
  • Assuming template validates against the live cluster — it doesn't, unless --validate is added explicitly; a chart referencing a nonexistent CRD renders "successfully" without it.
  • Forgetting helm get values shows merged values, not your -f file's raw content — the distinction matters when a chart's own values.yaml defaults are silently still in effect for keys you never overrode.
  • Using update when you meant build (or vice versa) for chart dependencies — see the dependency management NOTE above.

When to reach for something else#

For raw manifest customization without a templating engine or a packaged distribution model, Kustomize (see its own cheat sheet) is the more natural fit — patch-based overlays on plain YAML, no Go-template syntax to learn. Many real platforms use both: Helm for third-party/vendored charts (databases, ingress controllers), Kustomize for first-party application manifests layered with environment-specific overlays.