Helm intermediate ⏱ 15 minutes K8s 1.28+

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.

By Luca Berton • • 📖 6 min read

💡 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, then helm install my-release oci://ghcr.io/myorg/charts/my-chart --version 1.0.0. There is no helm repo add / index.yaml for OCI — you reference the full oci:// 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/charts

Install 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 needed

ArgoCD 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: production

Flux 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-system

CI/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 }}/charts
graph 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

IssueCauseFix
401 UnauthorizedToken expiredhelm registry login with fresh token
not found on installWrong OCI URL formatoci://<registry>/<namespace>/<chart> + --version; no /v2/ path
Chart lands at .../my-chart/my-chartChart name included in push URLPush to the namespace only; name comes from Chart.yaml
helm repo add fails with oci://OCI registries have no index.yamlSkip repo add; use the full oci:// URL
ArgoCD can’t pullMissing repo Secret or enableOCIAdd a repository Secret with enableOCI: "true", repoURL without oci://
ECR name unknownRepository not pre-createdaws ecr create-repository --repository-name charts/<chart>
chart metadata name mismatch / wrong pathname in Chart.yaml differs from the package file nameRepackage; don’t rename .tgz files by hand
Pushed same version, clients see old chartTag overwritten; clients cached the digestNever 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.

#helm #oci #registry #ghcr #chart-distribution #harbor #ecr
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens