Building a home for charts: the index.yaml structure, the various repository types from HTTP to ChartMuseum, GitHub Pages-based repositories with GitHub Actions automation, and how to manage chart publication consistently.

After episode 16, where we packaged the chart into a signed .tgz artifact with its version frozen via Chart.lock — in this episode we build the storage home for that artifact: a chart repository. This is the point where your chart stops being a local file and becomes a product anyone can use.
Why is this topic important? Until now, your colleagues have had to copy the chart folder via git or ZIP to install it — primitive, prone to stale versions, and impossible to automate. A repository changes all of that: users just helm repo add once, then helm search repo and helm install whenever they want, with version constraints. This is what lets ecosystems like Bitnami, ingress-nginx, and prometheus-community be consumed by the whole world with a single command. Without a repository, Helm is merely a sophisticated templating engine; with one, it becomes a real package manager.
In this episode we dissect the repository's anatomy — especially index.yaml as its heart — then walk through the commonly used repository types, build a simple HTTP repository, automate publication to GitHub Pages with GitHub Actions, and close with ChartMuseum for more enterprise needs.
Technically, a chart repository is just a static folder accessible over HTTP(S), containing two things: .tgz chart files and an index.yaml. No special server, no database — just files. index.yaml is the catalog telling Helm: what charts are available, which versions, and at which URL each version can be downloaded.
When you run helm search repo bitnami/nginx, Helm doesn't traverse the server — it downloads index.yaml from the added repository, stores it in the local cache (~/.cache/helm/repository/), and searches within that list. Same with helm install with a version constraint: Helm picks a matching version directly from the catalog, then downloads its .tgz. That means index.yaml must always be in sync with the chart files in the folder — which is what helm repo index maintains.
An example of index.yaml after generation:
apiVersion: v1
entries:
myapp:
- apiVersion: v2
appVersion: "1.27.3"
created: "2026-08-02T10:15:00Z"
description: myapp application chart
digest: 1f7a4b3c9d2e0f1a8b6c4d2e0f1a8b6c
name: myapp
urls:
- myapp-2.0.0.tgz
version: 2.0.0
- apiVersion: v2
appVersion: "1.25.0"
created: "2026-05-10T08:00:00Z"
description: myapp application chart
digest: 9d2e0f1a8b6c4d2e0f1a8b6c1f7a4b3c
name: myapp
urls:
- myapp-1.4.0.tgz
version: 1.4.0
generated: "2026-08-02T10:16:00Z"Notice the urls section — here the URLs are relative (myapp-2.0.0.tgz), and Helm combines them with the repository's base URL. You can also write absolute URLs, for example when the chart is stored in a separate bucket. The digest section is the SHA-256 of the .tgz archive — this is what lets Helm detect a corrupt archive or one that doesn't match the catalog.
Because a repository is just a "folder + index.yaml accessible over HTTP," almost any file storage location can become a repository. Choosing the type is an architecture decision that affects how your team works, so understand the trade-offs of each:
| Type | How it works | Suitable for | Drawbacks |
|---|---|---|---|
| Static HTTP/HTTPS | Ordinary folder on a web server / CDN | Public chart publishing, lightweight distribution | Manual index management, hard auth |
| GitHub Pages | The gh-pages branch served as a static site | Open-source projects, zero-cost needs | Not ideal for private internal use |
| S3 / GCS / Azure Blob | Object storage bucket served statically | Large scale, internal, cloud-integrated | Needs access configuration & tooling |
| OCI registry | Charts stored as images in a registry (Helm 3.8+) | Modern enterprise, single auth path with images | No index.yaml, needs OCI tooling (episode 18) |
| ChartMuseum | Dedicated server with upload/delete API | Enterprise, needs auth & multi-tenancy | One more component to operate |
An important pattern to remember: the simpler, the less to operate. For public charts, static GitHub Pages is more than enough; for private internal use, a restricted-access S3 bucket or ChartMuseum makes more sense. Don't add server complexity before auth, multi-tenancy, or API needs actually exist.
To understand the essence, let's build a repository from scratch — it only takes three steps: prepare a folder, place the charts, then generate the index.
# 1. Prepare a folder and copy the chart archives into it
mkdir -p ~/charts
cp myapp-2.0.0.tgz myapp-1.4.0.tgz ~/charts/
# 2. Generate index.yaml from the folder contents
cd ~/charts
helm repo index .
# 3. Serve the folder as a static site
python3 -m http.server 8080Now, from another machine (or another terminal), the repository is already usable:
helm repo add myorg http://localhost:8080
helm search repo myorg
helm install myapp myorg/myapp --version 2.0.0This demonstrates something important: a Helm repository is a very thin concept. There's no logic on the server — all the intelligence lives in the client. The server's only obligation is to serve index.yaml and the .tgz files correctly.
When should you update index.yaml? Every time a new chart comes in. Two ways: regenerate the whole folder (helm repo index .), which is safe and idempotent, or use the --merge flag to combine the old index with the new — useful when you're only adding one new chart version without reconsidering other entries. The rule of thumb: in human hands, a full helm repo index . is safer because there's no risk of a wrong merge.
Before going further, open the freshly generated index.yaml for a moment — understanding its contents is the key to diagnosing repository problems:
apiVersion: v1
entries:
myapp:
- apiVersion: v2
description: Demo application for the Helm series
name: myapp
urls:
- myapp-2.0.0.tgz
version: 2.0.0
digest: 8f9a3b... # sha256 of the manifest, not the chartThe three fields that most often cause problems: urls determines the relative or absolute path to the .tgz file — if you move a chart to a subfolder, update this field so clients can find the archive; version is the chart version users pin (helm install ... --version 2.0.0); and digest is the sha256 of the chart manifest (not of the raw .tgz file) — when helm install finds a digest that doesn't match the .tgz contents, it refuses to install and shows an error "sha256 ... does not match". This is why the index must always be regenerated after files change: a stale digest makes charts uninstallable.
Tip
One classic trap: uploading the .tgz to the folder but forgetting to run helm repo index. Users who run helm repo update will still see the old chart list — or worse, the new chart appears in the listing but helm install fails because the digest doesn't match. Make "always regenerate the index after adding files" a ritual that can't be skipped; the automation in the next section exists precisely to make sure this ritual is never missed.
The HTTP repository above works, but there's a practical problem: a machine must stay running. The free and most popular solution for public charts is GitHub Pages — GitHub provides static hosting for every repository, and since a chart repository is a static folder, it's a perfect fit.
The strategy: create two branches in one repository. The main branch stores the chart source code (the charts/ folder and the workflow). The gh-pages branch stores the publication result — a folder containing .tgz files and index.yaml ready to be served by Pages. Every time something changes in main, GitHub Actions packages the charts and writes the result to gh-pages.
To avoid writing all these steps yourself (which is error-prone), the Helm ecosystem provides chart-releaser-action (from helm/chart-releaser). This action reads all the charts in a given folder, compares versions against what's already published, packages the new ones, and regenerates index.yaml automatically. The complete workflow:
name: Release Charts
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "chart-releaser"
git config user.email "chart-releaser@users.noreply.github.com"
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.6.0
with:
charts_dir: charts
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"What this workflow does: on every push to main, chart-releaser-action scans charts/, finds charts whose versions haven't been released yet, packages them, commits the result to the gh-pages branch, and updates index.yaml there. Chart publication becomes reproducible and automatic — no more manual steps that can be forgotten.
To use a published repository:
helm repo add myorg https://<username>.github.io/<repo-name>
helm search repo myorg
helm repo updateThe Pages URL is usually of the form https://<username>.github.io/<repo-name>. One note: private repos can use Pages only with certain paid plans, so the GitHub Pages pattern naturally suits charts that are indeed public — for internal charts, go back to a bucket or ChartMuseum.
If your needs go beyond a "static folder" — for example, you need to upload charts via an API from CI, delete versions, authentication, or multi-tenancy — it's time to consider ChartMuseum (github.com/helm/chartmuseum). It's a Go server that acts as a repository with an HTTP API interface on top.
Installation can be via binary, container, or its own Helm chart directly:
docker run --rm -d \
--name chartmuseum \
-p 8080:8080 \
-e STORAGE=local \
-e STORAGE_LOCAL_ROOTDIR=/charts \
-v "$(pwd)/charts:/charts" \
ghcr.io/helm/chartmuseum:latestWith ChartMuseum, uploading a chart no longer means "putting a file in a folder" — it's done via the API:
# Upload a chart
curl -L --data-binary "@myapp-2.0.0.tgz" \
http://localhost:8080/api/charts
# List the charts in the repo
curl http://localhost:8080/api/charts/myapp
# Delete a single version
curl -X DELETE http://localhost:8080/api/charts/myapp/1.4.0The key point: ChartMuseum automatically keeps index.yaml in sync with every API operation. Upload a chart → the index immediately contains the new entry; delete a version → the index is updated accordingly. This is what makes it suitable for pipelines: CI can curl the chart upload without manually managing the index.
The features that make it enterprise-ready:
AUTH_ANONYMOUS_GET for public reads, then protect write operations), or integrate with OpenID Connect/JWT for more granular control./org-a/, /org-b/), so a single instance can serve multiple teams with chart separation.local, it supports S3, GCS, Azure Blob, and OSS. This lets ChartMuseum run on ephemeral servers while chart data lives in durable object storage.When should you choose ChartMuseum over GitHub Pages? The answer is when non-functional needs arise: you need an API for upload/delete automation, need auth, need multi-tenant, or want chart data in managed storage. For everything else, the simpler static structure is better — one less thing to operate and to be attacked.
In this episode 17 we built a home for your charts. We understood that a chart repository is technically just a static folder containing .tgz files and index.yaml — and that all the version resolution intelligence lives in the Helm client, not the server. We compared repository types from static HTTP, GitHub Pages, cloud object storage, OCI registry, to ChartMuseum, complete with their trade-offs. We built an HTTP repository from scratch with helm repo index, automated publication to GitHub Pages using chart-releaser-action so index.yaml always stays in sync without human intervention, and closed with ChartMuseum, which adds an API, authentication, multi-tenancy, and managed storage backends on top of the same concept.
The core takeaways:
index.yaml; the resolution intelligence lives in the client.helm repo index is the sync keeper; never forget to run it after adding charts.chart-releaser-action gives free, automatic chart publication for public projects.helm repo update must be run by users so the latest index.yaml reaches the local cache.Now your charts have a home and can be consumed by anyone. But there's one repository type we left in the table earlier — OCI registry — which fundamentally changes how Helm stores and verifies charts. In the next episode, episode 18, we dissect OCI support: the helm push/helm pull operations, the oci:// URL scheme, registry authentication, Cosign signing integration, and a migration strategy from HTTP repositories. Keep your spirits up!