Helm Values, Upgrades, and Rollbacks

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Override Helm chart configuration with values files and --set, understand precedence, upgrade safely with --atomic, and roll back when a release goes wrong.

Values are how one chart serves every environment. Upgrades are how you change a running release. Rollbacks are how you undo it.

These three are where most day-to-day Helm friction lives, and most of it comes from not knowing exactly which values ended up applied.

Start by reading the defaults

helm show values bitnami/nginx > values.yaml

Charts frequently expose more than a hundred settings, and the shipped defaults target a demo. One replica, no resource limits, persistence disabled. Skipping this step is how a production install ends up with an unbounded pod and no storage.

Overriding

A values file: the form to use for anything real, because it is reviewable and belongs in version control:

helm install myapp ./mychart -f production-values.yaml

You can pass several, and later files win on a per-key basis:

helm install myapp ./mychart -f base.yaml -f production.yaml -f secrets.yaml

This layering is the clean way to handle environments: a shared base plus a small per-environment overlay, rather than four near-identical files that drift apart.

--set for one-off values, typically an image tag from CI:

helm upgrade --install myapp ./mychart \
  -f production.yaml \
  --set image.tag=$GIT_SHA

--set has sharp edges worth knowing:

# Commas separate values, so commas inside a value must be escaped
--set nodeSelector."kubernetes\.io/os"=linux

# Dots in a key must be escaped too
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod

# Arrays use braces
--set ingress.hosts={api.example.com,www.example.com}

# Force a string. Otherwise "01" becomes 1 and a version tag breaks
--set-string image.tag=01

--set-string exists because --set infers types. An image tag like 1.10 parsed as a float becomes 1.1, which fails to pull with a confusing error.

For values containing secrets, --set-file reads from a file so nothing lands in shell history or CI logs.

Precedence

From lowest to highest:

  1. Subchart values.yaml
  2. Parent chart values.yaml
  3. -f files, in the order given. Later wins
  4. --set and --set-string. Highest

Merging is per key, deep for maps. A values file that sets only replicaCount leaves everything else at the chart defaults.

Arrays are replaced entirely, not merged. Overriding one element of a list means supplying the whole list. This catches people with ingress.hosts and extraEnv constantly.

When the result is not what you expect, stop guessing:

# Exactly what you supplied
helm get values myapp

# The full merged result, defaults included
helm get values myapp --all

That second command answers nearly every "why is this setting wrong" question in one step.

Upgrading

helm upgrade --install myapp ./mychart -f production.yaml --atomic --timeout 5m

Four things in that command, each earning its place:

FlagWhy
--installIdempotent: works on first run and every run after
-f production.yamlExplicit values, in version control
--atomicRoll back automatically if the release does not become ready
--timeout 5m--atomic implies --wait; without a timeout it waits on a stuck rollout far too long

--atomic is the single most valuable flag in Helm. Without it, a failed upgrade leaves the release partly applied, some resources new, some old, status failed, and you have to reason about a half-state during an incident. With it, Helm reverts and you are back where you started.

--reuse-values and --reset-values

A genuine trap. By default, helm upgrade discards previously supplied values that are not passed again, so an upgrade that only passes --set image.tag=x silently loses every other override you made at install time.

# Keep previous values, apply new ones on top
helm upgrade myapp ./mychart --reuse-values --set image.tag=1.5.0

# Discard previous values, go back to chart defaults plus what is passed now
helm upgrade myapp ./mychart --reset-values -f production.yaml

The reliable way to avoid thinking about this at all: always pass the complete values file on every upgrade, and keep that file in Git. Then the command is deterministic and --reuse-values never comes up.

Preview first

helm diff upgrade myapp ./mychart -f production.yaml   # helm-diff plugin
helm upgrade myapp ./mychart -f production.yaml --dry-run

The helm-diff plugin shows a real diff against the live release and is worth installing before you need it.

Rolling back

helm history myapp
REVISION  UPDATED                  STATUS      CHART          APP VERSION  DESCRIPTION
1         Mon Sep  8 09:12:03 2026 superseded  mychart-1.3.0  2.6.0        Install complete
2         Wed Sep 10 14:31:55 2026 superseded  mychart-1.4.0  2.7.0        Upgrade complete
3         Fri Sep 12 08:02:17 2026 deployed    mychart-1.5.0  2.8.0        Upgrade complete
helm rollback myapp 2      # to a specific revision
helm rollback myapp        # to the immediately previous one

A rollback creates a new revision rather than deleting history. Rolling back from 3 to 2 produces revision 4 with revision 2's content. History stays complete and auditable.

Three limits worth stating plainly:

  • It reverts Kubernetes objects only. A database migration run by the bad release is still applied.
  • Deleted data is gone. If the upgrade removed a PVC, rollback recreates the claim, not the contents.
  • History is bounded by --history-max, which defaults to 10.

When a release is stuck

An upgrade interrupted partway can leave a release in pending-upgrade, and Helm will refuse to do anything else with it:

helm status myapp
# Roll forward to a known-good state
helm rollback myapp <last-good-revision>

If rollback itself refuses, the release Secret holding the pending state can be removed so Helm reverts to the last completed revision:

kubectl get secret -n <namespace> -l owner=helm,name=myapp
kubectl delete secret sh.helm.release.v1.myapp.v<stuck-revision> -n <namespace>

Do that only when you are certain no upgrade is still running, and check the actual cluster objects afterwards.

Quick reference

TaskCommand
Read chart defaultshelm show values <chart>
Install with valueshelm install <r> <chart> -f values.yaml
Idempotent deployhelm upgrade --install <r> <chart> -f values.yaml --atomic
One-off override--set key=value / --set-string key=value
Values you suppliedhelm get values <r>
Full merged valueshelm get values <r> --all
Rendered manifestshelm get manifest <r>
Historyhelm history <r>
Roll backhelm rollback <r> [revision]
Preview a changehelm diff upgrade <r> <chart> -f values.yaml

Key takeaways

  • Read helm show values before installing. Chart defaults are demo defaults.
  • Precedence is chart → -f files in order → --set. Maps merge deeply; arrays are replaced wholesale.
  • helm get values --all settles any argument about which value actually applied.
  • --atomic with a --timeout on every upgrade, so a failure reverts instead of leaving a half-state.
  • Upgrades discard previous values by default. Pass the full values file every time and the problem disappears.
  • Use --set-string for image tags: --set will turn 1.10 into 1.1.
  • Rollback reverts objects, not migrations or deleted data.

Check your understanding

Exercise

Settings that vanished

A release was installed with -f production.yaml. A later upgrade ran with only --set image.tag=1.5.0. Afterwards, replica count and resource limits are back at chart defaults. Why?

Exercise

An override that lost entries

A chart default sets ingress.hosts to three hostnames. Your values file specifies one. After install, only your single host is configured. Is this expected?


Frequently Asked Questions

What is the order of precedence for Helm values?

From lowest to highest: subchart values.yaml, then the parent chart's values.yaml, then each -f file in the order given with later files winning, then --set and --set-string. Merging is deep for maps, so a file setting only one key leaves the rest of the defaults intact. Arrays are the exception. A higher-precedence list replaces the lower one entirely rather than merging. helm get values <release> --all shows the final merged result.

Why did my helm upgrade lose the values I set at install time?

Because helm upgrade computes values from the chart defaults plus what you pass on that specific command; anything supplied previously and not repeated is dropped. Passing only --set image.tag=x therefore reverts every other override. --reuse-values carries the previous set forward, but the robust habit is to keep a values file in Git and pass it in full on every upgrade, so the command is deterministic and does not depend on release state.

What does --atomic do and should I always use it?

--atomic makes an upgrade all-or-nothing: Helm waits for the resources to become ready and, if they do not, automatically rolls the release back to the previous revision. Without it a failed upgrade leaves the release partially applied with some resources new and some old, which is a bad state to reason about during an incident. Use it on every upgrade, and pair it with --timeout because --atomic implies --wait and will otherwise wait a long time on a stuck rollout.

Why does my image tag break when I use --set?

--set infers types from the value, so 1.10 is parsed as a float and becomes 1.1, and 01 becomes 1. The resulting image reference does not exist and the pull fails with an error that gives no hint about the cause. Use --set-string image.tag=1.10 to force string handling. The same inference affects values like true, false and anything that looks numeric, so --set-string is the safer choice for any value that must stay textual.

How do I roll back a Helm release, and what are the limits?

helm history <release> lists revisions and helm rollback <release> <revision> reverts to one, creating a new revision rather than erasing history. The limits are important: it reverts Kubernetes objects only, so a database migration applied by the bad release remains applied, and data removed with a PersistentVolumeClaim is not restored by recreating the claim. History is also capped by --history-max, which defaults to 10, so very old revisions are not available indefinitely.

How should I structure values files for multiple environments?

Keep a shared base file with everything common, then a small per-environment overlay holding only what differs, and pass both: -f base.yaml -f production.yaml. Later files override earlier ones per key, so the overlay stays short and the shared settings have exactly one definition. This avoids the usual failure mode of three or four near-identical full values files that quietly drift apart. Keep secrets out of these files entirely. Use --set-file, or an external secret manager synced into the cluster.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources