Helm OCI Registry: Push, Pull and Install Charts
Push, pull and install Helm charts from OCI registries (GHCR, ECR, ACR, Harbor): helm registry login, helm push, Argo CD and Flux OCI sources, CI release.
💡 Quick Answer: Since Helm 3.8 OCI support is GA:
helm registry login ghcr.io,helm package ./my-chart,helm push my-chart-1.0.0.tgz oci://ghcr.io/myorg/charts, thenhelm install my-release oci://ghcr.io/myorg/charts/my-chart --version 1.0.0. There is nohelm repo add/index.yamlfor OCI — you reference the fulloci://URL every time. Works with GHCR, ECR, ACR, GAR, Harbor, Quay and Artifactory.Gotcha: The chart name and version come from Chart.yaml; the push URL is only the namespace (
oci://ghcr.io/myorg/charts, not.../charts/my-chart).
The Problem
Traditional Helm chart repositories (ChartMuseum, GitHub Pages index.yaml) are separate infrastructure to maintain. OCI registries (the same ones storing your container images) now support Helm charts natively — one registry for everything, with built-in auth, replication, and vulnerability scanning.
The Solution
Push Charts to OCI Registries
# Package the chart
helm package ./my-chart
# Creates: my-chart-1.0.0.tgz
# Login to registry (stdin keeps the token out of shell history)
echo "$GITHUB_TOKEN" | helm registry login ghcr.io -u myuser --password-stdin
# ECR: aws ecr get-login-password --region us-east-1 | helm registry login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
# ACR: az acr login --name myregistry (or helm registry login myregistry.azurecr.io)
# Quay: echo "$QUAY_TOKEN" | helm registry login quay.io -u "$QUAY_USER" --password-stdin
# Push to GHCR
helm push my-chart-1.0.0.tgz oci://ghcr.io/myorg/charts
# Push to ECR (the repository <namespace>/<chart-name> must exist first)
aws ecr create-repository --repository-name charts/my-chart
helm push my-chart-1.0.0.tgz oci://123456789.dkr.ecr.us-east-1.amazonaws.com/charts
# Push to ACR
helm push my-chart-1.0.0.tgz oci://myregistry.azurecr.io/charts
# Push to Harbor
helm push my-chart-1.0.0.tgz oci://harbor.example.com/chartsInstall from OCI
# Install directly from OCI
helm install my-release oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
# Pull locally first
helm pull oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
# Show chart info
helm show chart oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
helm show values oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
helm show readme oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
# Template without installing
helm template my-release oci://ghcr.io/myorg/charts/my-chart --version 1.0.0
# Pin by digest (immutable) — the digest is printed by helm push
helm install my-release oci://ghcr.io/myorg/charts/my-chart@sha256:<digest>OCI Charts as Dependencies
# Chart.yaml
dependencies:
- name: common
version: "2.x.x"
repository: "oci://ghcr.io/myorg/charts"helm dependency update ./my-app # pulls into charts/ — no helm repo add neededArgoCD with OCI Helm Charts
repoURL has no oci:// prefix for Helm-type sources; private registries need a repository Secret with enableOCI:
apiVersion: v1
kind: Secret
metadata:
name: ghcr-charts
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: helm
name: ghcr-charts
url: ghcr.io/myorg/charts
enableOCI: "true"
username: myuser
password: <token>
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
source:
chart: my-chart
repoURL: ghcr.io/myorg/charts
targetRevision: 1.0.0
helm:
values: |
replicaCount: 3
image:
tag: v2.0.0
destination:
server: https://kubernetes.default.svc
namespace: productionFlux with OCI Helm Charts
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: my-charts
namespace: flux-system
spec:
type: oci
interval: 5m
url: oci://ghcr.io/myorg/charts
secretRef:
name: ghcr-credentials
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: my-app
namespace: production
spec:
interval: 5m
chart:
spec:
chart: my-chart
version: "1.0.0"
sourceRef:
kind: HelmRepository
name: my-charts
namespace: flux-systemCI/CD Release Pipeline
#!/bin/bash
# release-chart.sh — Automated OCI chart release
set -euo pipefail
CHART_DIR="$1"
REGISTRY="oci://ghcr.io/myorg/charts"
# Get version from Chart.yaml
VERSION=$(grep '^version:' "$CHART_DIR/Chart.yaml" | awk '{print $2}')
CHART_NAME=$(grep '^name:' "$CHART_DIR/Chart.yaml" | awk '{print $2}')
echo "Releasing $CHART_NAME v$VERSION to $REGISTRY"
# Lint
helm lint "$CHART_DIR" --strict
# Package
helm package "$CHART_DIR"
# Push
helm push "${CHART_NAME}-${VERSION}.tgz" "$REGISTRY"
# Verify
helm show chart "$REGISTRY/$CHART_NAME" --version "$VERSION"
echo "Released $CHART_NAME v$VERSION"GitHub Actions on tag push (GHCR, no PAT needed):
name: Publish Helm Chart
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v4
- run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- run: helm package ./charts/my-chart
- run: helm push my-chart-*.tgz oci://ghcr.io/${{ github.repository_owner }}/chartsgraph TD
A[helm package] --> B[my-chart-1.0.0.tgz]
B --> C[helm push to OCI registry]
C --> D[GHCR / ECR / ACR / Harbor]
D --> E[ArgoCD / Flux / helm install]
D --> F[Vulnerability scanning]
D --> G[Cross-region replication]Common Issues
| Issue | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Token expired | helm registry login with fresh token |
not found on install | Wrong OCI URL format | oci://<registry>/<namespace>/<chart> + --version; no /v2/ path |
Chart lands at .../my-chart/my-chart | Chart name included in push URL | Push to the namespace only; name comes from Chart.yaml |
helm repo add fails with oci:// | OCI registries have no index.yaml | Skip repo add; use the full oci:// URL |
| ArgoCD can’t pull | Missing repo Secret or enableOCI | Add a repository Secret with enableOCI: "true", repoURL without oci:// |
ECR name unknown | Repository not pre-created | aws ecr create-repository --repository-name charts/<chart> |
chart metadata name mismatch / wrong path | name in Chart.yaml differs from the package file name | Repackage; don’t rename .tgz files by hand |
| Pushed same version, clients see old chart | Tag overwritten; clients cached the digest | Never re-push a version; enable tag immutability in the registry |
Best Practices
- One registry for images and charts — simplifies auth and management
- Immutable versions — most registries allow overwriting a tag by default; turn on tag immutability (ECR, Harbor, ACR) and bump the version instead
- Sign charts with cosign for supply chain security
- Use digest pinning in production ArgoCD/Flux manifests
- Automate releases in CI — no manual
helm push
Key Takeaways
- OCI registries replace ChartMuseum as the standard for Helm chart distribution
- Same auth, replication, and scanning as container images
- ArgoCD and Flux both support OCI Helm sources natively
- Enable tag immutability and pin digests to prevent accidental overwrites
Frequently Asked Questions
Can I use helm repo add with an OCI registry?
No. helm repo add expects an HTTP repository with index.yaml. OCI charts are referenced directly: helm install x oci://registry/namespace/chart --version 1.2.3, and as repository: "oci://registry/namespace" in Chart.yaml dependencies.
Which Helm version supports OCI?
OCI support became stable (no HELM_EXPERIMENTAL_OCI) in Helm 3.8.0. Any current Helm 3.x or 4.x release supports push, pull, install, template and dependencies from OCI.
How do I list chart versions in an OCI registry?
Helm has no search for OCI. Use the registry’s UI/API, oras repo tags ghcr.io/myorg/charts/my-chart, or crane ls.
How do I sign and verify OCI charts?
Sign the pushed artifact with cosign (cosign sign ghcr.io/myorg/charts/my-chart@sha256:<digest>) and verify in CI or with an admission policy (Kyverno, Sigstore policy-controller). Flux can verify cosign signatures on OCIRepository/HelmRepository sources.

Recommended
Kubernetes Recipes — The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book →Learn by Doing
CopyPasteLearn — Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses →🎓 Deepen Your Skills — Hands-on Courses
Courses by CopyPasteLearn.com — Learn IT by Doing
