---
title: 'helm upgrade --install: Idempotent Deploys (2026)'
description: helm upgrade --install installs a release if it's missing and upgrades it if it exists — one idempotent command. See the Helm 4 flags, gotchas, and CI pattern.
url: https://nerdleveltech.com/helm-upgrade-install-idempotent-deploy
language: en
date: '2026-07-01'
category: cloud-devops
tags:
  - helm
  - helm 4
  - kubernetes
  - helm upgrade install
  - ci-cd
  - devops
  - cloud-native
markdown_url: https://nerdleveltech.com/helm-upgrade-install-idempotent-deploy.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/helm-upgrade-install-idempotent-deploy'
terms: https://nerdleveltech.com/llms.txt
---

# helm upgrade --install: Idempotent Deploys (2026)

`helm upgrade --install` is one command that installs a release if it doesn't exist yet and upgrades it if it does. The `--install` (`-i`) flag makes the deploy safe to re-run, which is why it's the standard idempotent command for CI/CD pipelines.[^upgrade][^bestpractices]

## TL;DR

Instead of branching between `helm install` (first deploy) and `helm upgrade` (every deploy after), you run a single `helm upgrade --install NAME CHART` everywhere. Helm checks whether a release with that name exists: if not, it installs; if so, it upgrades.[^upgrade] It is idempotent in the normal case, with one important exception — a *failed first install* leaves a stuck release and the next run errors with `has no deployed releases`.[^issue3353][^nodeployed]

This guide is written for **Helm 4** (GA November 12, 2025; latest patch 4.2.2 at the time of writing), which renamed several flags that older tutorials still get wrong — `--atomic` is now `--rollback-on-failure`, `--wait` and `--dry-run` take values, and `--force` became `--force-replace`.[^helm4][^upgrade][^atomic]

## What you'll learn

- What `helm upgrade --install` actually does and why it's idempotent
- When to reach for `helm install` vs `helm upgrade`
- The exact reason `helm upgrade --install` sometimes fails with `has no deployed releases`, and how to fix it
- How `--create-namespace` works with `--install`
- What changed for these flags in Helm 4
- `--reuse-values` vs `--reset-values` vs `--reset-then-reuse-values` and the "my values disappeared" trap
- How to preview and auto-roll-back a deploy with `--dry-run`, `--wait`, and `--rollback-on-failure`
- A copy-paste CI/CD pattern for GitHub Actions

## What does `helm upgrade --install` do?

It runs an install when the release is absent and an upgrade when it's present — in a single command. The behavior comes entirely from the `-i, --install` flag, which the official reference defines as "if a release by this name doesn't already exist, run an install."[^upgrade]

First, confirm you're on Helm 4:

```bash
helm version
# version.BuildInfo{Version:"v4.2.2", ...}
```

Then deploy the same way whether it's the first time or the fiftieth:

```bash
helm upgrade --install myapp ./charts/myapp \
  --namespace prod --create-namespace
```

On the first run Helm creates release `myapp` at revision 1. On later runs it computes a diff and bumps the revision.[^using] Every install, upgrade, or rollback increments the revision by 1, and Helm keeps the last 10 revisions per release by default (`--history-max`, `0` for unlimited).[^upgrade] You can inspect the history at any time:

```bash
helm history myapp -n prod
helm status myapp -n prod
```

## helm install vs helm upgrade: when do you use each?

Use `helm install` for a brand-new release and `helm upgrade` to change an existing one — but in automation you rarely want to choose. `helm install` fails if the release name is already taken, and a plain `helm upgrade` fails if the release doesn't exist yet.[^using] `helm upgrade --install` collapses both into one command that never cares which situation it's in, which is exactly what you want in a script that might run against a fresh cluster or an existing one.[^bestpractices]

There is still a place for the standalone commands. Reach for `helm install` when you explicitly want the run to fail loudly if the release already exists (for example, to guard against clobbering something), and for `helm upgrade` without `--install` when you want a deploy to fail rather than silently create a release that was supposed to already be there.

## Is `helm upgrade --install` idempotent?

Mostly yes — running it repeatedly with the same inputs converges on the same state — but there's one sharp edge. The `-i` flag only triggers an *install* when **no release exists at all**. If your very first `helm upgrade --install` fails partway through, Helm still records a release, just with a status of `failed` or `pending-install` rather than `deployed`.[^issue3353]

On the next run, `--install` sees that a release named `myapp` already exists, so it takes the *upgrade* path. Upgrade needs a revision in the `deployed` state to use as its baseline, finds none, and stops:

```text
Error: UPGRADE FAILED: "myapp" has no deployed releases
```

So the command is idempotent once you have a healthy first deployment, but a broken first install can wedge it. The next section covers the fix.

## Why does `helm upgrade --install` fail with "has no deployed releases"?

Because `helm upgrade` requires a previous revision whose status is `deployed` to diff against, and after a failed first install there isn't one.[^nodeployed] When every stored revision is `failed`, `pending-install`, or `pending-upgrade`, Helm has no baseline and refuses to continue. This guard was added back in Helm 2.7.1 and is still present in Helm 3 and Helm 4.[^nodeployed]

You have three ways out, from cleanest to hackiest:

```bash
# 1. Cleanest: remove the stuck release, then let --install do a fresh install
helm uninstall myapp -n prod
helm upgrade --install myapp ./charts/myapp -n prod

# 2. Prevent it next time: roll back automatically on failure (see below)
helm upgrade --install myapp ./charts/myapp -n prod --rollback-on-failure

# 3. Last resort: patch the release Secret's `status` label back to `deployed`
kubectl -n prod get secret -l owner=helm,name=myapp
# then edit the newest sh.helm.release.v1.myapp.v<N> secret's status label
```

Option 2 is the real prevention: if the *first* install fails and `--rollback-on-failure` is set, Helm uninstalls the failed release, leaving nothing stuck for the next run to trip over.[^upgrade] This error is especially common with Flux `HelmRelease` objects, where a single bad first reconcile leaves the release wedged until you uninstall it.[^nodeployed] If you run GitOps, our walkthrough of [GitOps workflow patterns from commit to cluster](https://nerdleveltech.com/mastering-gitops-workflow-patterns-from-commit-to-cluster) covers where this fits in a reconcile loop.

## How do I create the namespace with helm upgrade --install?

Add `--create-namespace`, which the docs define as "if `--install` is set, create the release namespace if not present."[^upgrade] It only does anything alongside `--install`, so it pairs naturally with this command:

```bash
helm upgrade --install myapp ./charts/myapp \
  --namespace prod --create-namespace
```

Without it, deploying into a namespace that doesn't exist yet fails. This is a common first-run stumble, because plain `helm install` in Helm 2 used to create namespaces implicitly, whereas Helm 3 and 4 return an error to match the behavior of other Kubernetes tooling.[^changes]

## What changed for these flags in Helm 4?

Helm 4 renamed or restructured several flags you almost always pair with `helm upgrade --install`, and older guides still show the Helm 3 spellings.[^helm4][^upgrade] Here's the mapping that matters:

| Helm 3 | Helm 4 | Notes |
|---|---|---|
| `--atomic` | `--rollback-on-failure` | Old `--atomic` still works on `upgrade` but is deprecated in favor of `--rollback-on-failure`. A regression that broke the deprecated `--atomic` on `helm install` (an "unknown flag" error) was fixed in 4.1.3.[^atomic] |
| `--wait` (boolean) | `--wait WaitStrategy` | Values: `watcher`, `hookOnly`, `legacy`. Bare `--wait` = `watcher`; omitting the flag defaults to `hookOnly`.[^upgrade] |
| `--dry-run` (boolean) | `--dry-run string` | Values: `none` (default), `client`, `server`.[^upgrade] |
| `--force` | `--force-replace` | Plus a new `--force-conflicts` for server-side apply conflicts.[^upgrade] |

Two of these are easy to get subtly wrong. First, `--wait` no longer means "wait for my Deployment to be ready" by default — if you omit it, Helm 4 uses the `hookOnly` strategy and does **not** wait for your workload pods.[^upgrade] Pass `--wait` explicitly to get the kstatus-based `watcher` behavior that waits for resources to become ready.[^helm4] Second, Helm 4 added server-side apply: `helm install` defaults `--server-side` to `true`, while `helm upgrade` defaults it to `auto` (inheriting the previous release's method).[^install][^upgrade]

## `--reuse-values` vs `--reset-values` vs `--reset-then-reuse-values`

These three flags control which values an upgrade starts from, and the default behavior surprises people. Here's what each one does, per the reference:[^upgrade]

- `--reset-values`: reset to the values built into the chart, ignoring what the last release used.
- `--reuse-values`: reuse the last release's values and merge in any `--set` / `-f` overrides from this command.
- `--reset-then-reuse-values`: reset to the chart's values, re-apply the last release's values, then merge this command's overrides. Added in Helm 3.14.0.

The trap is the **default** when you pass none of them. If your upgrade command includes any value flag (`--set`, `--set-string`, `--set-file`, `--values`/`-f`), Helm implicitly uses the *reset* strategy — so any ad-hoc values you set on a previous deploy but didn't repeat this time are wiped.[^reusevalues] If you pass no value flags at all, Helm implicitly *reuses* the previous values.[^reusevalues]

```bash
# Deploy 1: set a replica count
helm upgrade --install myapp ./charts/myapp -n prod --set replicaCount=4

# Deploy 2: change only the image tag, WITHOUT repeating replicaCount
helm upgrade --install myapp ./charts/myapp -n prod --set image.tag=1.5.0
# replicaCount silently reverts to the chart default here (reset strategy)
```

`--reuse-values` avoids that revert, but has its own catch: it also ignores any *new* keys added to the chart's `values.yaml` in the version you're upgrading to. `--reset-then-reuse-values` is usually what people actually want — it keeps your previous overrides while still picking up new chart defaults.[^reusevalues] The most robust fix, though, is to stop relying on `--set` and keep every override in a version-controlled values file that you pass with `-f` on every deploy.

## How do I preview an upgrade before it happens?

Use `--dry-run=client` to render the release without touching the cluster, optionally with `--debug` to see the full manifest. In Helm 4 `--dry-run` takes a mode: `client` "simulates the operation client-side only and avoids cluster connections," while `server` runs the simulation against the API server.[^upgrade]

```bash
# Client-side render, no cluster connection needed
helm upgrade --install myapp ./charts/myapp -n prod \
  --dry-run=client --debug

# Hide rendered Secrets in the output
helm upgrade --install myapp ./charts/myapp -n prod \
  --dry-run=client --hide-secret
```

For a pure template render with no release logic at all, `helm template ./charts/myapp` also works entirely offline.[^using]

## How do I make a failed deploy roll back automatically?

Combine `--wait` with `--rollback-on-failure` and a `--timeout`. With `--rollback-on-failure` set, "Helm will rollback the upgrade to previous success release upon failure," and it automatically defaults `--wait` to the `watcher` strategy so Helm actually watches your resources become ready before declaring success.[^upgrade]

```bash
helm upgrade --install myapp ./charts/myapp \
  --namespace prod --create-namespace \
  --wait --rollback-on-failure --timeout 5m
```

If the pods never reach ready within `--timeout` (default `5m0s`), Helm marks the release failed and rolls back to the last good revision.[^upgrade] For deployments where you also spin up Jobs, add `--wait-for-jobs` so Helm waits for those Jobs to complete too.[^upgrade] This pairs well with the readiness practices in our guide to [zero-downtime Kubernetes deployments](https://nerdleveltech.com/kubernetes-zero-downtime-deployments-tutorial).

## How do I use helm upgrade --install in CI/CD?

Run the exact same command in your pipeline that you'd run by hand — its idempotency is what makes it CI-safe.[^bestpractices] Because the command behaves identically whether the release exists or not, you never need conditional logic in your workflow.

```yaml
name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Install Helm 4
        run: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash
      - name: Deploy (idempotent)
        run: |
          helm upgrade --install myapp ./charts/myapp \
            --namespace prod --create-namespace \
            --wait --rollback-on-failure --timeout 5m
```

One CI gotcha worth knowing: the popular `azure/setup-helm` action installs Helm on the runner, but its README states "v2+ of this action only support Helm3," and it falls back to a hardcoded default of `v3.18.4` if version detection fails.[^setuphelm] If you specifically need Helm 4 on the runner, install it with the official `get-helm-4` script as shown above rather than relying on the action's default.[^install2] For structuring the surrounding pipeline, see our comparison of [reusable workflows vs composite actions in GitHub Actions](https://nerdleveltech.com/github-actions-reusable-workflow-vs-composite-action).

## FAQ

**Does `helm upgrade --install` create the release if it doesn't exist?**
Yes. The `-i, --install` flag runs an install when no release by that name exists, and an upgrade when one does.[^upgrade]

**Is `helm upgrade --install` idempotent?**
In the normal case, yes — re-running it converges to the same state. The exception is a failed *first* install, which leaves a non-`deployed` release and causes the next run to error with `has no deployed releases`.[^issue3353][^nodeployed]

**How do I fix `UPGRADE FAILED: has no deployed releases`?**
Uninstall the stuck release (`helm uninstall NAME -n NS`) and re-run `helm upgrade --install`, or prevent it entirely by adding `--rollback-on-failure` so a failed first install is cleaned up automatically.[^nodeployed][^upgrade]

**What replaced `--atomic` in Helm 4?**
`--rollback-on-failure`. The old `--atomic` still works on `helm upgrade` but is deprecated; a regression affecting deprecated `--atomic` on `helm install` was fixed in Helm 4.1.3.[^atomic]

**Why did my previously set values disappear after an upgrade?**
Because passing any `--set`/`-f` value flag makes Helm default to the *reset* strategy, dropping earlier ad-hoc overrides you didn't repeat. Use `--reset-then-reuse-values`, or keep all overrides in a values file you pass every time.[^reusevalues]

**Does `--create-namespace` work without `--install`?**
No — it only creates the namespace "if `--install` is set," so it's meant to be used with `helm upgrade --install`.[^upgrade]

**How do I preview the change without applying it?**
Use `--dry-run=client` (optionally with `--debug`), which renders the release without connecting to the cluster.[^upgrade]

## Bottom line

`helm upgrade --install` is the right default for almost every deploy: one idempotent command that installs or upgrades without you branching on the release's current state.[^bestpractices] On Helm 4, pair it with `--create-namespace` for first runs, `--wait --rollback-on-failure --timeout` for safe automated deploys, and a version-controlled `-f values.yaml` to sidestep the value-merge surprises. The two things that bite people — the `has no deployed releases` wedge after a failed first install, and the renamed Helm 4 flags — are both avoidable once you know they exist.

Next, harden the deploy itself with [zero-downtime Kubernetes deployment strategies](https://nerdleveltech.com/kubernetes-zero-downtime-deployments-tutorial), wire it into a pipeline with [reusable GitHub Actions workflows](https://nerdleveltech.com/github-actions-reusable-workflow-vs-composite-action), or manage it declaratively using [GitOps commit-to-cluster patterns](https://nerdleveltech.com/mastering-gitops-workflow-patterns-from-commit-to-cluster).

[^helm4]: Helm, "Helm 4 Released" (November 17, 2025). https://helm.sh/blog/helm-4-released/
[^upgrade]: Helm, "helm upgrade" command reference (v4.2.2). https://helm.sh/docs/helm/helm_upgrade/
[^install]: Helm, "helm install" command reference (v4). https://helm.sh/docs/helm/helm_install/
[^using]: Helm, "Using Helm." https://helm.sh/docs/intro/using_helm/
[^nodeployed]: phoenixNAP, "Fix 'helm has no deployed releases' Error." https://phoenixnap.com/kb/helm-has-no-deployed-releases
[^issue3353]: helm/helm issue #3353, "`helm upgrade --install` doesn't perform an install/upgrade if the first ever install fails." https://github.com/helm/helm/issues/3353
[^atomic]: helm/helm issue #31900, "helm install with --atomic no longer works in Helm 4." https://github.com/helm/helm/issues/31900
[^reusevalues]: H. Cao, "Understand helm upgrade flags — reset-values & reuse-values." https://medium.com/@kcatstack/understand-helm-upgrade-flags-reset-values-reuse-values-6e58ac8f127e
[^install2]: Helm, "Installing Helm." https://helm.sh/docs/intro/install/
[^setuphelm]: Azure/setup-helm, GitHub Action README. https://github.com/Azure/setup-helm
[^bestpractices]: Coder Society, "13 Best Practices for using Helm." https://codersociety.com/blog/articles/helm-best-practices
[^changes]: Helm, "Changes Since Helm 2" (namespace creation). https://helm.sh/docs/v3/faq/changes_since_helm2/
