A chart in a directory is a project. A chart in a registry, versioned and signed, is a product other people can depend on. The difference is a handful of commands and one decision about where it lives.
Topic 1: Packaging
helm package ./checkout
# → checkout-0.4.1.tgz ← the CHART version, not appVersion
helm package ./checkout --version 0.4.2 --app-version 1.6.1
helm package ./checkout --dependency-update
The filename comes from name and version in Chart.yaml. Packaging the same version twice with different content is the mistake that breaks consumer caches — every content change needs a version bump, and CI should enforce it.
.helmignore decides what is excluded, and getting it wrong ships things you did not intend:
.git/
.github/
*.tgz
values-local.yaml
ci/
tests/
.vscode/
After packaging, look inside before you publish. It takes five seconds and it is how you find the values-dev.yaml with a password in it:
tar -tzf checkout-0.4.1.tgz
Topic 2: OCI Registries — the Current Answer
Charts are OCI artifacts. They live in the same registry as your images, with the same authentication, the same access control and the same scanning:
helm registry login ghcr.io -u USERNAME --password-stdin <<< "$GITHUB_TOKEN"
helm push checkout-0.4.1.tgz oci://ghcr.io/acme/charts
# → Pushed: ghcr.io/acme/charts/checkout:0.4.1
# Digest: sha256:9f8e7d...
helm install checkout oci://ghcr.io/acme/charts/checkout --version 0.4.1
helm upgrade --install checkout oci://ghcr.io/acme/charts/checkout --version 0.4.1
helm show values oci://ghcr.io/acme/charts/checkout --version 0.4.1
helm pull oci://ghcr.io/acme/charts/checkout --version 0.4.1 --untar
What this removes: no index.yaml to generate and keep consistent, no separate chart server to run and back up, no second set of credentials. What it adds: charts appear in your registry’s UI, retention policies and vulnerability scanning alongside the images they deploy.
As a dependency, an OCI repository looks the same as any other:
dependencies:
- name: common
version: 1.2.0
repository: oci://ghcr.io/acme/charts
Note there is no helm repo add for OCI — you reference the full URL, and helm registry login handles auth. That difference trips people who expect the repo-list workflow.
Topic 3: The Classic Repository, Because You Will Meet It
A Helm repository is a directory of .tgz files plus a generated index.yaml:
mkdir -p repo && cp checkout-0.4.1.tgz repo/
helm repo index repo --url https://charts.acme.example
helm repo index repo --url https://charts.acme.example --merge repo/index.yaml
# index.yaml
apiVersion: v1
entries:
checkout:
- apiVersion: v2
name: checkout
version: 0.4.1
appVersion: "1.6.0"
created: "2026-08-18T09:12:00Z"
digest: sha256:9f8e7d...
urls: [https://charts.acme.example/checkout-0.4.1.tgz]
generated: "2026-08-18T09:12:00Z"
Consumers use the familiar flow:
helm repo add acme https://charts.acme.example
helm repo update
helm search repo acme/
helm install checkout acme/checkout --version 0.4.1
Hosting options in ascending order of effort: GitHub Pages with the chart-releaser action (which packages, publishes and updates index.yaml on every tag), an S3 or GCS bucket with a plugin, or a full artifact repository like Harbor, Artifactory or Nexus.
helm repo update is a local cache refresh. “The new version does not exist” from a consumer is usually a stale cache, and it is the first thing to check.
Topic 4: Provenance and Signing
A .prov file contains the chart’s hash and a signature over it:
gpg --full-generate-key
gpg --export-secret-keys > ~/.gnupg/secring.gpg # Helm still wants the legacy format
helm package ./checkout --sign --key 'release@acme.example' \
--keyring ~/.gnupg/secring.gpg
# → checkout-0.4.1.tgz
# checkout-0.4.1.tgz.prov
helm verify checkout-0.4.1.tgz --keyring ~/.gnupg/pubring.gpg
helm install checkout acme/checkout --verify --keyring ~/.gnupg/pubring.gpg
--verify refuses to install unless the signature checks out — which is the point: a consumer can prove the chart came from you and has not been altered, without trusting the transport or the registry.
Cosign is the pragmatic alternative for OCI-hosted charts, and it is what most teams already run for images:
cosign sign ghcr.io/acme/charts/checkout:0.4.1
cosign verify ghcr.io/acme/charts/checkout:0.4.1 \
--certificate-identity-regexp '.*' --certificate-oidc-issuer https://token.actions.githubusercontent.com
Keyless signing ties the signature to a CI identity rather than to a key somebody has to store — one tool and one policy covering both images and charts, which is a materially simpler thing to operate than a GPG keyring per release engineer.
Pin by digest where certainty matters. A version tag can be overwritten in most registries; a digest cannot:
helm install checkout oci://ghcr.io/acme/charts/checkout@sha256:9f8e7d...
Topic 5: Releasing a Chart From CI
The pipeline that makes chart releases boring:
# on: push tags 'checkout-v*'
steps:
- uses: actions/checkout@v4
- name: Lint and template every values shape
run: |
helm lint charts/checkout --strict
for f in charts/checkout/ci/*-values.yaml; do
helm template checkout charts/checkout -f "$f" > /dev/null
done
- name: Install into kind and run chart tests
run: |
kind create cluster
helm install checkout charts/checkout --wait --timeout 5m
helm test checkout
- name: Package and push
run: |
helm dependency build charts/checkout
helm package charts/checkout --destination dist/
helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin <<< "${{ secrets.GITHUB_TOKEN }}"
helm push dist/checkout-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts
Three properties worth building in from the start:
- The version comes from the tag, so a release cannot be published without a version bump.
helm dependency build, notupdate— publish exactly what the lock file pins.- Install into a throwaway cluster and run
helm testbefore publishing. A chart that has never been installed by CI is a chart nobody has verified.
For teams on GitHub Pages, chart-releaser (cr) does the packaging, release creation and index.yaml update in one action, and is worth using rather than scripting the same steps.
Topic 6: Consuming Third-Party Charts Safely
helm show chart bitnami/postgresql # metadata, maintainers, version
helm show values bitnami/postgresql | less # the interface
helm pull bitnami/postgresql --version 15.5.0 --untar
helm template test ./postgresql | grep -E 'image:|kind: (ClusterRole|Role)|privileged|hostPath'
The four questions to answer before a public chart runs in production:
- Which images, from which registry? Mirror them into your own registry if you have one.
- What RBAC does it request? Cluster-scoped permissions need a justification.
- Does it mount anything from the host?
hostPath,hostNetwork,privileged— all worth reading in the rendered output rather than in the values file. - Is it maintained? An abandoned chart pins an abandoned image with abandoned CVEs.
Mirror what you depend on. A public chart repository going away, rate-limiting, or removing an old version is a real outage during an incident — and mirroring into your own OCI registry is one helm pull and one helm push.
Try it yourself: publish a chart to a local registry:2 container, install from it, then modify a byte in the .tgz and run helm verify. Watching the verification fail is what makes provenance concrete rather than ceremonial.
Common mistake: publishing a new chart build under a version that already exists. Consumers who cached the old one get different content under the same version, helm dependency build produces different results on different machines, and the resulting “works on my cluster” is genuinely hard to diagnose. Enforce a version bump in CI — a three-line check comparing the chart version against published tags prevents it permanently.