A Complete Guide to Building and Hosting Helm Charts
If you are managing Kubernetes applications, manually handling dozens of YAML files quickly becomes unscalable. This is where Helm comes in.
In this guide, we will cover what Helm is, how to create a custom chart, and how to host it on your own private OCI-compliant registry running inside your cluster.
1. The Theory: Helm and OCI
What is a Helm Chart?
Think of Helm as the package manager for Kubernetes (like apt for Ubuntu). A Helm Chart is the package. Instead of static YAML files, Helm uses templates. You feed these templates a values.yaml file to dynamically configure your application for different environments (dev, staging, prod) without rewriting the core code.
What is OCI?
OCI stands for the Open Container Initiative. It is the universal standard for container formats. Modern Helm fully supports OCI registries. This means the exact same registry you use to store Docker images (like Docker Hub, Harbor, or a private registry) can also store and distribute your Helm charts.
2. Creating Your First Helm Chart
We will start by generating a boilerplate chart and customizing it.
Step 1: Generate the Chart
Use the Helm CLI to create a scaffolding directory named my-app.
Bash
helm create my-app
Step 2: Customize the Chart
Navigate into the my-app directory. Open the values.yaml file to declare your variables. For example, to run two instances of your application:
YAML
# values.yaml
replicaCount: 2
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "latest"
Helm will inject these values into the templates located in the /templates directory during deployment.
Step 3: Lint and Test
Verify your chart has no syntax errors and perform a dry run to see the rendered YAML:
Bash
helm lint ./my-app
helm template ./my-app
3. Setting Up a Private OCI Registry on Kubernetes
Instead of using a third-party service, let's deploy the official Docker Registry (v2) into our cluster. We will secure it with basic authentication and back it with persistent storage.
Step 1: Generate Authentication Credentials
Use htpasswd locally to generate an encrypted password file.
Bash
htpasswd -Bc auth your-username
Create a Kubernetes Secret from this file so the registry pod can use it:
Bash
kubectl create secret generic registry-auth --from-file=htpasswd=auth
Step 2: Provision Persistent Storage (PVC)
Ensure your charts are not lost if the pod restarts. We are requesting 10GB using a local storage class (adjust storageClassName based on your cluster, such as local-path for RKE2).
YAML
# registry-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
Step 3: Deploy the Registry, Service, and Ingress
Here is the complete manifest to deploy the registry, attach the storage, enforce authentication, and expose it to the internet.
YAML
# registry.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: private-registry-deployment
labels:
app: private-registry
spec:
replicas: 1
selector:
matchLabels:
app: private-registry
template:
metadata:
labels:
app: private-registry
spec:
containers:
- name: registry
image: registry:2
ports:
- containerPort: 5000
env:
- name: REGISTRY_STORAGE_DELETE_ENABLED
value: "true"
- name: REGISTRY_AUTH
value: "htpasswd"
- name: REGISTRY_AUTH_HTPASSWD_REALM
value: "Registry Realm"
- name: REGISTRY_AUTH_HTPASSWD_PATH
value: "/auth/htpasswd"
volumeMounts:
- name: registry-storage
mountPath: /var/lib/registry
- name: auth-volume
mountPath: /auth
readOnly: true
volumes:
- name: registry-storage
persistentVolumeClaim:
claimName: registry-pvc
- name: auth-volume
secret:
secretName: registry-auth
---
apiVersion: v1
kind: Service
metadata:
name: private-registry-service
spec:
selector:
app: private-registry
ports:
- protocol: TCP
port: 80
targetPort: 5000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: private-registry-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
ingressClassName: nginx
tls:
- hosts:
- registry.yourdomain.com
secretName: registry-tls-secret
rules:
- host: registry.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: private-registry-service
port:
number: 80
Apply the manifests:
Bash
kubectl apply -f registry-pvc.yaml
kubectl apply -f registry.yaml
4. Pushing Your Chart to the Registry
With your OCI registry live at registry.yourdomain.com, you can now package and push your chart exactly like a Docker image.
Step 1: Package the Chart
Bundle your chart directory into a single archive (it will output a file like my-app-0.1.0.tgz).
Bash
helm package ./my-app
Step 2: Log in to the Registry
Authenticate using the credentials you generated earlier.
Bash
helm registry login registry.yourdomain.com -u your-username
Step 3: Push the Chart
Push the packaged archive to the registry under a clean /charts path.
Bash
helm push my-app-0.1.0.tgz oci://registry.yourdomain.com/charts
5. Deploying from Your Private Registry
Your chart is now safely stored in your OCI registry. You, or your CI/CD pipelines, can install it directly onto any cluster without needing the local source files.
Bash
helm install my-release oci://registry.yourdomain.com/charts/my-app --version 0.1.0
To verify the deployment:
Bash
helm ls
kubectl get pods
When you are done, Helm makes cleanup effortless. A single command removes all resources tied to that release:
Bash
helm uninstall my-release
Conclusion
By treating Helm charts as standard OCI artifacts, you streamline your deployment pipelines and consolidate your infrastructure. You no longer need separate systems for container images and Kubernetes manifests—everything lives securely in one place.