Learn Helm Chart - Chart Packaging & Versioning
Episode 16 of 30

Learn Helm Chart - Chart Packaging & Versioning

Mastering the chart release cycle: disciplined Semantic Versioning, the version vs appVersion difference, packaging charts with helm package, GPG provenance signing, and the Chart.lock that guarantees reproducible dependencies.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After episode 15, where we built production-grade chart documentation — README.md as the contract, NOTES.txt as the post-installation assistant, automatic parameter table generation with helm-docs, and CHANGELOG.md as the transparency record — in this episode we turn the chart from just a folder of templates into a distributable artifact: a .tgz archive with a clear version, a verifiable signature, and reproducible dependencies.

Why is this topic crucial? Because up until this episode, your chart has only lived in one local folder. But in the real world, charts aren't run — they're consumed. Other teams install them, CI uses them, pipelines release them into production clusters. For all of that to be safe, three questions must be answered definitively: which version is currently in use? Is this artifact authentic and unmodified along the way? Can this build be reproduced? The first answer is handled by Semantic Versioning, the second by provenance signing, and the third by Chart.lock.

In this episode we dissect all three one by one. We start with disciplined semver rules and the subtle difference between version and appVersion, then package the chart with helm package, sign it with GPG and verify it, and close with the dependency locking mechanism that makes chart builds deterministic.

Main Discussion

Semantic Versioning — A Language Agreed Upon by Machines and Humans

Semantic Versioning (semver) is a MAJOR.MINOR.PATCH numbering standard that turns version numbers into a contract: each number conveys meaning about the impact of a change on users. This isn't just a tidy convention — it's risk communication.

  • MAJOR increments when there are breaking changes. Removing a replicas parameter others use, changing a long-standing values structure, bumping to an incompatible apiVersion — all of these increment MAJOR. Chart consumers using a ^1.x constraint will not automatically receive 2.x.
  • MINOR increments when there are new features that remain backward compatible. Adding a new autoscaling.enabled parameter doesn't break anyone — those using the old defaults won't feel a thing. This bumps MINOR.
  • PATCH increments when there are bug fixes that don't change public behavior. Fixing a template typo that produced a wrong label is a PATCH.

The golden rule: if there's any doubt whether a change is breaking, it's a MAJOR. Why does this discipline matter? Because chart users stake their applications on version constraints. Imagine you're using the constraint >=1.0.0 <2.0.0 and the chart team bumps the version to 1.5.0 while actually containing a breaking change. That version passes the constraint, your production pipeline automatically picks it up, and your application explodes without warning. Undisciplined semver isn't just a writing error — it's a lie that destroys trust.

Tip

Apply the "public vs private" rule: semver governs the public contractvalues.yaml parameters, generated resource names, rendered templates. Internal changes like a _helpers.tpl refactor that doesn't change rendered output may bump PATCH. The most objective way to know is to compare the rendered output of the old version vs the new: if helm template produces different YAML for the same input values, that's a behavior change.

version vs appVersion — Two Different Clocks

One of the classic Helm confusions is two fields with similar names but different rhythms: version and appVersion in Chart.yaml.

  • version is the chart package's version — the number Helm uses for semver constraints, caching, and chart selection in a repository. This is what you see in helm search repo and helm list. It changes according to the semver rules above.
  • appVersion is the version of the application packaged inside the chart — for example, nginx:1.27.3. It's informational, not decisive: Helm never uses appVersion for version resolution logic. It only appears in chart metadata and is usually rendered into the app.kubernetes.io/version label.

The key point: these two versions are independent. Your chart can be at version: 2.0.0 while appVersion: 1.27.3. Why not sync them? Because charts and applications have different lifecycles: you can release chart 1.0.0 packaging nginx 1.25, then release chart 1.1.0 packaging nginx 1.27 (new feature: application upgrade). Both numbers go up, but at their own pace.

Chart.yaml - two independent versions
apiVersion: v2
name: myapp
description: myapp application chart
type: application
version: 2.3.1          # chart package version (semver, used by Helm)
appVersion: "1.27.3"   # nginx application version (informational)

Notice that appVersion is written in quotes. This is important: a value like 1.10 will be coerced by YAML into the number 1.1 without quotes — a silently wrong application version. Always write appVersion as a string.

helm package — Packaging a Chart Into a Single Artifact

The chart folder we've been working on can't be distributed yet — it contains many files and directories. helm package combines everything into a single .tgz archive named chart-name-version.tgz. This is the chart distribution unit: one file, the version in the filename, ready to be uploaded to a repository or pulled with helm pull.

Packaging a chart
helm package ./myapp
 
# Output:
# Successfully packaged chart and saved it to: /home/user/myapp-0.1.0.tgz

Notice the archive details. The .tgz file stores the chart's folder name, not the archive's filename. Inside the archive there's a myapp/ folder containing the entire chart contents. The folder name inside the archive is what determines the chart's name when installed — if you rename the folder inside the archive, Helm will treat the chart as having a different name. And because the format is tar.gz, you can inspect its contents directly:

Inspecting a chart archive's contents
tar -tzf myapp-0.1.0.tgz
 
# myapp/Chart.yaml
# myapp/values.yaml
# myapp/templates/deployment.yaml
# myapp/templates/_helpers.tpl
# myapp/README.md

Before packaging, make a habit of two rituals. First, run helm lint to make sure the chart has no structural problems. Second, use .helmignore to exclude files that don't need to enter the archive — local test files, values-dev.yaml, screenshots, .git — so the archive is clean and doesn't contain information that shouldn't go out. Every file that gets past .helmignore is shipped to users; files that shouldn't circulate here are a security risk.

Provenance & Signing — Proving a Chart Wasn't Tampered With

Now the chart is a single file. The next question: how do users know this file really came from you and wasn't modified by someone else along the way? The answer is provenance — proof of origin.

When you sign a chart, helm package produces a second file named myapp-0.1.0.tgz.prov. This file contains the SHA-256 digest of the chart archive along with its metadata, signed with your GPG key. Users holding your public key can verify that the archive is exactly as it was when signed — one bit changes, and verification fails.

The process starts by preparing a GPG key:

Creating a GPG key for chart signing
gpg --quick-generate-key "Helm Chart Signing" rsa4096 sign 0
 
# Store the passphrase in a file (don't commit it to the repo!)
echo -n "your-passphrase" > /tmp/signing-key.pass
 
gpg --batch --pinentry-mode loopback --yes \
  --export-secret-key --armor "Helm Chart Signing" > /tmp/signing-key.gpg
 
gpg --batch --pinentry-mode loopback --import /tmp/signing-key.gpg

Then sign while packaging:

Packaging with signing
helm package ./myapp \
  --sign \
  --key "Helm Chart Signing" \
  --keyring ~/.gnupg/secring.gpg \
  --passphrase-file /tmp/signing-key.pass
 
# ls: myapp-0.1.0.tgz  myapp-0.1.0.tgz.prov

Users who receive the chart can verify its authenticity with helm verify:

Verifying a signed chart
helm verify myapp-0.1.0.tgz
# Signed by: Helm Chart Signing <...>
# Using Key With Fingerprint: ABCD...
# Chart Hash Verified: myapp-0.1.0.tgz

Important

helm verify only proves that the signature matches a key you hold in your keyring — it does not judge whether that key is trustworthy. Trust establishment is a separate process: you must obtain the chart author's public key through a secure channel, verify its fingerprint out of band, and store it once. This is exactly the "trust on first use" pattern used by SSH: pin the fingerprint the first time, then all subsequent verifications refer to it. Distributing the public key through the same chart repo as the chart itself destroys its security value — like keeping a padlock's key inside the padlock.

Let's be honest about adoption: .prov provenance signing is still rarely used in the classic Helm ecosystem, and for charts distributed via OCI registries modern practice has shifted toward Cosign (which we'll cover in episode 18). But the .prov mechanism is still important to understand because it's the same conceptual foundation: separating the artifact from its proof of authenticity.

Chart.lock — Reproducible Dependencies

The last question this episode answers: are chart builds reproducible? This is a problem every package manager faces — and the solution is the same: a lock file.

Remember in episode 11 we added dependencies with version constraints in Chart.yaml, for example version: "~1.2.0". A constraint like that is a range: ~1.2.0 means "latest 1.2.x". Every time helm dependency update runs, Helm might pull version 1.2.9 today and 1.2.12 next month — the rendered chart output could differ, behavior could change, and bugs that weren't there yesterday can appear tomorrow. For production, that's unacceptable.

The solution is Chart.lock — a file generated by helm dependency update that records the exact version of every resolved dependency. Just like package-lock.json in Node.js or the bun.lock you use in this project, it freezes the resolution result so that two people running helm dependency build on different machines (or two weeks apart) get an identical dependency set.

Chart.lock
dependencies:
- name: redis
  repository: https://charts.bitnami.com/bitnami
  version: 19.6.4
digest: sha256:1f7a4b3c...9d2e0f1a
generated: "2026-08-02T10:15:00Z"

Three important fields here: version, which is now an exact version (not a constraint), digest, the SHA-256 that guarantees the dependency's contents haven't changed, and generated, the creation timestamp. The workflow is simple:

  • When adding/changing a dependency: edit Chart.yaml, then run helm dependency update to regenerate Chart.lock, and commit both.
  • When building from scratch (new machine, CI): run helm dependency build — this reads Chart.lock and pulls the exact recorded versions, without re-resolving.
Updating dependencies then building from lock
# After changing Chart.yaml:
helm dependency update ./myapp
 
# In CI / on a new machine - using the committed Chart.lock:
helm dependency build ./myapp

There's a consequence you should be aware of: helm dependency update always re-resolves and overwrites Chart.lock; helm dependency build respects the existing lock. If Chart.yaml changes but Chart.lock isn't committed along with it, helm dependency build will fail with a warning that they're out of sync — that's a feature, not a bug: it forces you to handle dependency changes explicitly. Reproducibility isn't free; it's a discipline maintained by a mechanism.

Warning

Never put Chart.lock in .gitignore unless your chart intentionally wants to resolve dependencies dynamically on every build (a pattern that only suits development charts, not releases). For distributed charts, the committed Chart.lock is the source of truth: without it, "the same build" is a meaningless phrase.

The complete chart release workflow from this episode: bump version in Chart.yaml per semver rules → helm linthelm dependency updatehelm package --sign → verify with helm verify. The result: one .tgz archive with a deterministic name, one .prov file as proof of authenticity, and a Chart.lock guaranteeing everyone builds from the same dependency set.

Conclusion

In this episode 16 we turned the chart into a distributable artifact. We understood Semantic Versioning as a risk language: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes — and that indiscipline here is a lie that destroys user trust. We distinguished version (the package version Helm uses for resolution) from appVersion (the informational application version), then packaged the chart into a .tgz with helm package, inspected it with tar -tzf, and cleaned it up with .helmignore. We signed the chart with GPG producing a .prov file, verified it with helm verify, and understood that trust is built through key fingerprints verified out of band. Finally, we froze dependencies with Chart.lock — split between helm dependency update (re-resolve) and helm dependency build (follow the lock) — so builds become reproducible.

The core takeaways:

  • Semver is a risk contract: when in doubt, it's a MAJOR.
  • version controls Helm resolution; appVersion is only information — write it as a string.
  • helm package --sign produces two artifacts: .tgz and .prov; helm verify checks the former against the latter.
  • Trust is built by verifying the key fingerprint, not by taking the key from the same source as the artifact.
  • Commit Chart.lock so dependency builds are deterministic.

Now you have an artifact ready to be consumed — but one artifact sitting in a local folder can't yet be used by other teams. In the next episode, episode 17, we build a chart repository: the index.yaml structure, the various repository types from HTTP and GitHub Pages to ChartMuseum, and how to automate chart publication with GitHub Actions. Keep your spirits up!

Learn Helm Chart - Chart Packaging & Versioning | Learn Helm Chart