# Helm Cheat Sheet

> **Tool:** Helm
> **Category:** Containers & Orchestration
> **Verified against:** Helm v3.18.6, flags verified via `helm <cmd> --help` / `helm get --help` run locally, 2026-08-29
> **Official docs:** https://helm.sh/docs/helm/

## 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

```mermaid
flowchart LR
    A["Chart (templates + Chart.yaml)"] --> B["+ values.yaml / --set overrides"]
    B --> C["helm template renders Go templates → raw manifests"]
    C --> D{"install or upgrade?"}
    D -->|"install"| E["New Release, revision 1"]
    D -->|"upgrade"| F["Same Release, new revision"]
    E & F --> G["Stored release history<br/>(enables rollback)"]

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef ok fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    class A,B info
    class G ok
```

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

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

helm version
```

## Managing chart repositories

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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 🔍

```bash
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

```bash
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

```bash
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 🧪

```bash
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

```bash
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

```bash
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 add`ed 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:

```bash
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

```yaml
# .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.
