From local charts to distributed registries: packaging, publishing, and signing Helm charts.
helm package — creating .tgz archivesversion vs appVersionindex.yamloci:// protocol# Package a chart into a .tgz archive
helm package ./mychart/
# Output: mychart-1.2.0.tgz
# Package with explicit version override
helm package ./mychart/ --version 2.0.0-rc1
# Package into a specific directory
helm package ./mychart/ -d ./dist/
# Chart.yaml — two version fields
apiVersion: v2
name: mychart
version: 1.2.0 # Chart version (SemVer) — bump on every chart change
appVersion: "3.8.1" # App version — the software being deployed
# Add a chart repository
helm repo add bitnami https://charts.bitnami.com/bitnami
# Update local repo cache
helm repo update
# Search for charts
helm search repo bitnami/nginx
# Remove a repository
helm repo remove bitnami
# index.yaml — repository metadata
apiVersion: v1
entries:
mychart:
- name: mychart
version: 1.2.0
appVersion: "3.8.1"
created: "2026-04-01T10:00:00Z"
digest: sha256:abc123...
urls:
- https://charts.example.com/mychart-1.2.0.tgz
# Generate / update index.yaml
helm repo index ./dist/ --url https://charts.example.com
# Run ChartMuseum locally
docker run -d -p 8080:8080 \
-e STORAGE=local \
-e STORAGE_LOCAL_ROOTDIR=/charts \
-v $(pwd)/charts:/charts \
chartmuseum/chartmuseum
# Push a chart
curl --data-binary "@mychart-1.2.0.tgz" \
http://localhost:8080/api/charts
# Add as repo
helm repo add local \
http://localhost:8080
# Harbor supports both classic
# chart repos and OCI registries
# Classic: add Harbor as a repo
helm repo add harbor \
https://harbor.example.com/chartrepo/myproject \
--username admin --password secret
# OCI: push directly
helm push mychart-1.2.0.tgz \
oci://harbor.example.com/myproject
# Login to an OCI registry
helm registry login ghcr.io -u USERNAME
# Push a packaged chart to OCI
helm push mychart-1.2.0.tgz oci://ghcr.io/myorg/charts
# Pull a chart from OCI
helm pull oci://ghcr.io/myorg/charts/mychart --version 1.2.0
# Install directly from OCI
helm install myrelease oci://ghcr.io/myorg/charts/mychart --version 1.2.0
# 1. Package charts into docs/ or gh-pages branch
helm package ./mychart/ -d ./docs/
# 2. Generate index
helm repo index ./docs/ --url https://myorg.github.io/helm-charts
# 3. Push to gh-pages branch, then:
helm repo add myorg https://myorg.github.io/helm-charts
OCI registries (oci://) are the modern standard. They reuse existing container registry infrastructure — no separate index.yaml needed.
# Package and sign with a GPG key
helm package ./mychart/ --sign \
--key "Helm Signing Key" \
--keyring ~/.gnupg/secring.gpg
# Produces: mychart-1.2.0.tgz + mychart-1.2.0.tgz.prov
# Verify a chart before installing
helm verify mychart-1.2.0.tgz --keyring ~/.gnupg/pubring.gpg
# Install with verification
helm install myrelease mychart-1.2.0.tgz --verify \
--keyring ~/.gnupg/pubring.gpg
# .prov file (provenance) contents:
# - Chart metadata hash
# - Chart.yaml contents
# - PGP signature block
Provenance files ensure chart integrity and authenticity. In regulated environments, always sign charts in CI and verify on install.
# Typical CI/CD stages for Helm charts
# 1. Lint
helm lint ./mychart/ --strict
# 2. Template & validate
helm template ./mychart/ | kubeval --strict
# 3. Package
helm package ./mychart/ --version ${CI_TAG}
# 4. Push to registry
helm push mychart-${CI_TAG}.tgz oci://ghcr.io/myorg/charts
# .helmignore — files excluded from helm package
.git
.gitignore
.helmignore
*.md
tests/
ci/
.env
secrets.yaml
*.swp
*.bak
.helmignore works like .gitignore. Keep packages lean by excluding docs, tests, CI configs, and sensitive files.
.tgz archives from chart directoriesindex.yaml; managed by helm repo commandsoci:// protocol reuses container registry infrastructure--sign produces .prov files; --verify checks integrityNext: Lesson 19 →