Helm
.mdVerified 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#
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 versionManaging 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 reposInstalling 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 versionTip
-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 backTip
--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 diffhelm 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 itListing 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 integrityDebugging 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 callTip
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 testTest 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 defaultsNote
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 earliersearch 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 5mCommon pitfalls#
- Skipping
--atomicon a production upgrade — see the IMPORTANT callout above; this is the single highest-value flag on this whole page for production safety. - Assuming
templatevalidates against the live cluster — it doesn't, unless--validateis added explicitly; a chart referencing a nonexistent CRD renders "successfully" without it. - Forgetting
helm get valuesshows merged values, not your-ffile's raw content — the distinction matters when a chart's ownvalues.yamldefaults are silently still in effect for keys you never overrode. - Using
updatewhen you meantbuild(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.