If you have ever written a chart dependency as version: ~1.2.3 and wondered exactly which versions that will and will not pull in — or been surprised when ^0.2.3 refused to upgrade to 0.3.0 — you have run into Helm’s version constraint syntax. It is one of those features everybody uses and almost nobody reads the spec for, which is precisely why it bites.
Helm lets you pin chart versions with a small algebra of range operators: the tilde ~, the caret ^, wildcards (x, *), hyphen ranges, and boolean combinators. Get them right and your helm dependency update pulls exactly the patch releases you want and nothing that breaks you. Get them wrong and you either freeze on a stale sub-chart forever or let a breaking major slip into production.
This guide covers every operator with its exact bounds, the real-world use case for each, the two traps that catch most people (the 0.x caret behavior and silently-skipped pre-releases), and where these constraints are actually evaluated — not just in Chart.yaml dependencies, but on the helm install --version flag too. Everything here reflects current Helm 3 (and is unchanged in Helm 4).
The 30-Second Cheat Sheet
Before the details, here is every operator with its exact expansion. Pin this table.
| Constraint | Matches (inclusive/exclusive) | Plain English |
|---|---|---|
1.2.3 | exactly 1.2.3 | Exact pin |
=1.2.3 | exactly 1.2.3 | Same as above |
!=1.2.3 | anything except 1.2.3 | Exclude one version |
>=1.2.3, <2.0.0 | >= 1.2.3 and < 2.0.0 | Explicit range (AND) |
~1.2.3 | >= 1.2.3, < 1.3.0 | Patch updates only |
~1.2 | >= 1.2.0, < 1.3.0 | Patch updates only |
~1 | >= 1.0.0, < 2.0.0 | Any 1.x |
^1.2.3 | >= 1.2.3, < 2.0.0 | Minor + patch updates |
^0.2.3 | >= 0.2.3, < 0.3.0 | ⚠️ 0.x pins the minor |
^0.0.3 | >= 0.0.3, < 0.0.4 | ⚠️ 0.0.x pins the patch |
1.2.x | >= 1.2.0, < 1.3.0 | Wildcard patch |
1.x | >= 1.0.0, < 2.0.0 | Wildcard minor |
* | >= 0.0.0 | Anything (avoid) |
1.2.3 - 1.4.5 | >= 1.2.3, <= 1.4.5 | Inclusive both ends |
^1.0.0 || ^2.0.0 | any 1.x OR any 2.x | Boolean OR |
The rest of this article is each of these in depth, with the use case and the gotchas.
Where Helm Actually Uses These Constraints
Before the operators, know the two places this syntax is evaluated — they behave identically because they call the same parser:
1. Chart.yaml dependencies
Each entry under dependencies has a version field that “should contain a semantic version or version range”:
apiVersion: v2
name: my-app
version: 1.0.0
dependencies:
- name: postgresql
version: "~15.5.0"
repository: "https://charts.bitnami.com/bitnami"
- name: redis
version: "^20.0.0"
repository: "https://charts.bitnami.com/bitnami"2. The --version flag
The --version flag on helm install, upgrade, pull, template and friends. Straight from the CLI docs: “This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used.”
# Latest patch of 15.5
helm install db bitnami/postgresql --version "~15.5.0"
# Any 20.x, resolved at install time
helm upgrade cache bitnami/redis --version "^20.0.0"Under the hood, Helm 3 parses all of this with the Masterminds/semver v3 Go library (the same one Helm 4 ships). Every rule below comes from that library’s behavior, so it is consistent everywhere Helm accepts a version.
The Tilde ~: Patch Updates Only
The tilde is the conservative choice: allow patch releases, freeze the minor. It is the operator you want for a dependency you trust to follow SemVer but don’t want surprising you with new features (and new bugs) on a minor bump.
The rule: with a minor version specified, ~ allows patch-level changes; with only the major specified, it allows minor-level changes.
| Constraint | Expands to |
|---|---|
~1.2.3 | >= 1.2.3, < 1.3.0 |
~1.2 | >= 1.2.0, < 1.3.0 |
~1 | >= 1.0.0, < 2.0.0 |
~1.2.x | >= 1.2.0, < 1.3.0 |
Use case: a stateful dependency like a database chart where minor bumps can change StatefulSet fields, default storage, or init logic. version: "~15.5.0" says “give me 15.5.4, 15.5.9 — the bug fixes — but never jump me to 15.6 automatically.” This is the pattern Helm’s own dependencies best-practices page recommends as the default: version: ~1.2.3.
The Caret ^: Minor + Patch (and the 0.x Trap)
The caret is the “trust SemVer” operator: allow anything that shouldn’t break you — new minors and patches, but not a new major. For a well-behaved dependency past 1.0.0, this is the sweet spot between staying current and staying safe.
| Constraint | Expands to |
|---|---|
^1.2.3 | >= 1.2.3, < 2.0.0 |
^1.2 | >= 1.2.0, < 2.0.0 |
^1 | >= 1.0.0, < 2.0.0 |
Use case: a mature library chart (an ingress controller, a metrics exporter) that reliably reserves breaking changes for major bumps. version: "^20.0.0" keeps you on the latest 20.x without manual bumps, and stops cold at 21.0.0 so a breaking change never lands silently.
⚠️ The 0.x Caret Trap
Here is the single most misremembered rule in the whole system. Before a 1.0.0 release, the caret treats the minor number as the stability level, because in 0.x land every minor bump is allowed to break things. So:
| Constraint | Expands to | Not what you might expect |
|---|---|---|
^0.2.3 | >= 0.2.3, < 0.3.0 | not < 1.0.0 |
^0.2 | >= 0.2.0, < 0.3.0 | |
^0.0.3 | >= 0.0.3, < 0.0.4 | pins the patch |
^0.0 | >= 0.0.0, < 0.1.0 | |
^0 | >= 0.0.0, < 1.0.0 |
If you write ^0.2.3 expecting it to track every 0.x release up to 1.0.0, you will be quietly stuck on the 0.2 line — 0.3.0 will never be pulled. On a 0.x dependency, caret behaves essentially like tilde. Given how many CNCF-adjacent charts sit at 0.x for years, this is a real-world footgun, not a trivia question. If you genuinely want to float across 0.x minors, use an explicit range: >= 0.2.3, < 1.0.0.
Wildcards: x, X and *
Wildcards let you leave a position open. They work with the comparison operators too, and on a bare = they fall back to tilde-style patch matching.
| Constraint | Expands to |
|---|---|
1.2.x | >= 1.2.0, < 1.3.0 |
1.x | >= 1.0.0, < 2.0.0 |
>= 1.2.x | >= 1.2.0 |
<= 2.x | < 3.0.0 |
* | >= 0.0.0 (anything) |
Use case: 1.2.x reads more explicitly than ~1.2.0 to some teams and means the same thing — pick whichever your reviewers parse faster. A bare * matches everything and should be treated as a code smell in a committed Chart.yaml; it defeats the entire point of a lock and invites a breaking major on the next helm dependency update.
Hyphen Ranges: Inclusive Both Ends
A hyphen range gives you an explicit, inclusive window on both ends — handy when you know a dependency is good from version A through version B and you want to say exactly that.
| Constraint | Expands to |
|---|---|
1.2.3 - 1.4.5 | >= 1.2.3, <= 1.4.5 |
2.3.4 - 4.5 | >= 2.3.4, <= 4.5 |
⚠️ The whitespace matters. The spaces around the hyphen are mandatory. Written without them, 1.2.3-1.4.5 is not a range at all — it parses as the single version 1.2.3 with the pre-release identifier 1.4.5. That is a completely different (and almost certainly unintended) constraint, and it will fail to match silently. Always keep the spaces: 1.2.3 - 1.4.5.
Boolean Logic: AND, OR and Comparisons
You can combine constraints. Within a group, space and comma both mean AND — they are interchangeable. Groups are then joined with || for OR.
# AND — a bounded window
version: ">= 1.2.0, < 1.5.0"
# equivalent (space instead of comma)
version: ">= 1.2.0 < 1.5.0"
# OR — support two major lines at once
version: "^1.0.0 || ^2.0.0"
# real-world: pin a window but exclude one bad release
version: ">= 1.2.0, < 2.0.0, != 1.4.2"The full set of comparison operators is what you’d expect: =, !=, >, <, >=, <=. The != is underused and genuinely handy — when a specific patch ships a regression, != 1.4.2 skips exactly that one without abandoning your range.
Use case for ||: a chart that supports two major versions of a dependency during a migration window. ^1.0.0 || ^2.0.0 accepts either line, letting downstream users move at their own pace.
The Pre-Release Gotcha Nobody Reads
This one silently breaks CI pipelines. Ranges skip pre-release versions by default. A constraint like ~1.2.3 or >= 1.2.0 will not match 1.3.0-rc.1 or 1.2.5-beta.2, even though those versions are numerically inside the range.
The reason is deliberate: you rarely want a helm dependency update to pull a release candidate into a production chart. But it surprises people who tag pre-releases and wonder why Helm ignores them.
To opt in, add a pre-release comparator to the constraint itself — the idiomatic trick is appending -0:
# Ignores 1.2.4-rc.1 (default behavior)
version: "~1.2.3"
# Matches pre-releases too, e.g. 1.2.4-rc.1
version: "~1.2.3-0"The -0 works because pre-release identifiers sort in ASCII order and 0 is the lowest possible, so it acts as “any pre-release or higher.” One more sharp edge from the same rule: comparisons are ASCII-cased, so >= 1.2.3-BETA will match 1.2.3-alpha, because uppercase B sorts before lowercase a. If you use pre-release channels, keep the casing consistent.
Chart.yaml Range vs Chart.lock Pin: What Actually Reproduces a Build
A constraint is a range, not a pin — and understanding the difference between Chart.yaml and Chart.lock is what separates reproducible builds from “works on my machine.”
Chart.yamlholds the range (~15.5.0). It expresses intent: “acceptable versions.”Chart.lockholds the resolved, concrete versions plus a digest. It is generated, and it is what actually reproduces a build.
The two commands that interact with them do opposite things:
# Re-resolves the RANGE in Chart.yaml against the repo,
# picks concrete versions, and rewrites Chart.lock.
helm dependency update
# Ignores the range; rebuilds charts/ from the PIN in Chart.lock.
helm dependency buildThe practical rule: commit your Chart.lock. In CI, use helm dependency build so every pipeline run installs the exact versions the lock pins — the range in Chart.yaml only governs re-resolution when you deliberately run update. Treating the range as if it were the pin is how teams end up with subtly different sub-chart versions across environments. If you’re formalizing chart quality more broadly, this pairs well with a proper chart testing setup.
Since Which Helm Version?
Two things people conflate here — the syntax and the location:
- The constraint syntax itself is not new. Helm 2 already parsed the same Masterminds/semver ranges; it just kept dependencies in a separate
requirements.yamlfile. - What changed in Helm 3 is where dependencies live. With
apiVersion: v2charts (the Helm 3 format, released November 2019), dependencies moved intoChart.yaml. Helm’s docs are explicit: the change fromv1tov2“added adependenciesfield defining chart dependencies, which were located in a separaterequirements.yamlfile forv1charts.”
So if you are on any modern Helm (3.x or the new Helm 4), the full operator set in this article is available in your Chart.yaml and on --version. If you still maintain an ancient apiVersion: v1 chart, the same operators work — they just live in requirements.yaml. Set apiVersion: v2 and move the block into Chart.yaml when you migrate.
Which Operator Should You Use? A Decision Guide
| Your situation | Use |
|---|---|
| Production DB / stateful chart, want only bug fixes | ~1.2.3 (patch only) |
| Well-behaved library past 1.0.0, want to stay current | ^1.2.3 (minor + patch) |
A 0.x dependency you want to float across minors | explicit >= 0.2.3, < 1.0.0 — not ^0.2.3 |
| Exact reproducibility, no surprises | exact pin 1.2.3 + committed Chart.lock |
| Skip one known-bad release inside a range | >= 1.2.0, < 2.0.0, != 1.4.2 |
| Support two majors during a migration | ^1.0.0 || ^2.0.0 |
| You need release candidates | append -0, e.g. ~1.2.3-0 |
The honest default for most teams: use ~ (tilde) for stateful and infrastructure-critical dependencies where you want bug fixes and nothing else, use ^ (caret) for mature libraries you trust to respect SemVer, and always commit Chart.lock so the range never decides what actually deploys — the lock does.
Frequently Asked Questions
What does the tilde (~) mean in a Helm chart version?
The tilde allows patch-level updates and freezes the minor version: ~1.2.3 expands to >= 1.2.3, < 1.3.0, and ~1.2 means the same thing. With only a major specified, it widens one level (~1 is any 1.x). It is the operator Helm’s own dependency best-practices page recommends as the default, because it lets bug fixes through and blocks new minors that may change templates or defaults.
What does the caret (^) mean in Helm, and why does ^0.2.3 not match 0.3.0?
The caret allows minor and patch updates but never a new major: ^1.2.3 expands to >= 1.2.3, < 2.0.0. Below 1.0.0 the rule changes because in 0.x land any minor bump may break things, so the caret pins the left-most non-zero position: ^0.2.3 becomes >= 0.2.3, < 0.3.0 and ^0.0.3 becomes >= 0.0.3, < 0.0.4. If you want to float across 0.x minors, write the range explicitly: >= 0.2.3, < 1.0.0.
Can I use a version range with helm install –version?
Yes. The --version flag on helm install, helm upgrade, helm pull and helm template accepts the same constraint syntax as Chart.yaml dependencies, so --version "~15.5.0" installs the newest 15.5.x available in the repository at that moment. If the flag is omitted, Helm uses the latest non-pre-release version. Both places go through the same Masterminds/semver parser, so the expansion rules are identical.
Why does my Helm dependency ignore pre-release versions?
Because ranges skip pre-releases by default: ~1.2.3 will not match 1.2.4-rc.1 even though it is numerically inside the window. This is deliberate, so that helm dependency update never pulls a release candidate into a production chart by accident. To opt in, append a pre-release comparator to the constraint, typically -0, as in ~1.2.3-0, which matches any pre-release of the allowed versions as well as the final releases.
What is the difference between Chart.yaml and Chart.lock?
Chart.yaml stores the version range, which expresses which versions are acceptable; Chart.lock stores the concrete versions and digest that were actually resolved, and it is what reproduces a build. helm dependency update re-resolves the ranges and rewrites the lock, while helm dependency build ignores the ranges and rebuilds charts/ from the lock. Commit Chart.lock and use dependency build in CI so every pipeline installs exactly the same sub-chart versions.
Wrapping Up
Helm’s version constraints are a small language, but the two traps — caret pinning the minor on 0.x, and ranges silently skipping pre-releases — cause an outsized share of “why won’t it upgrade?” and “why did that break?” incidents. Internalize the cheat-sheet table, prefer ~ and ^ over bare wildcards, and let Chart.lock be the source of truth for reproducible builds.
For more on getting Helm right in production, see the companion guides on Helm values JSON schema validation, loading external files into ConfigMaps and Secrets, and what’s new in Helm 4.