Create and publish a Helm chart
Learn how to create and publish a Helm chart in this hands-on Kubernetes tutorial. Step-by-step guide with troubleshooting tips and next steps.
Focus: create and publish a helm chart
If you have ever deployed the same Kubernetes manifest across staging and production, only to find you missed a replica count or an image tag, you already understand the pain Helm solves. Without a templated, versioned packaging system, your YAML files become a swamp of copy-paste errors, inconsistent configs, and manual toil. This lesson teaches you how to create and publish a Helm chart — the standard way to bundle, configure, and distribute Kubernetes applications so your team can ship with confidence, not YAML fatigue.
The problem this lesson solves
Raw Kubernetes manifests are static. You cannot easily parameterise a Deployment spec for different environments, embed release metadata, or share a complete app stack with your colleagues without zipping and emailing YAML files. Helm charts solve this by introducing:
- Templates that inject values at install time (e.g.,
{{ .Values.replicaCount }}) - Built-in release tracking via Helm secrets
- Repository publishing so anyone can
helm installyour app - Dependency management for microservice stacks
Without a chart, your deployment process is fragile. With one, you unlock reproducibility, upgradability, and sharability — the difference between a script and a platform.
Core concept / mental model
Think of a Helm chart as a software installer for Kubernetes — like a .deb or .msi package, but for containerised apps. The chart is a directory structure that holds:
| Component | Purpose |
|---|---|
Chart.yaml |
Metadata (name, version, description) |
values.yaml |
Default configuration values |
templates/ |
Go-templated Kubernetes resource files |
charts/ |
Sub‑chart dependencies (optional) |
Mental model:
values.yamlis the control panel.templates/are the blueprints.helm installfeeds the control-panel settings into the blueprints to stamp out real Kubernetes objects.
How it works step by step
- Scaffold the chart structure with
helm create <chart-name>. Helm generates a starter skeleton with aDeployment,Service,HPA, andIngresstemplate. - Edit
Chart.yaml— setname,version,appVersion, and description. This metadata appears inhelm listand repository indexes. - Define defaults in
values.yaml— expose every tunable parameter: image registry, replica count, port, environment variables. - Write or modify templates — use Go’s template syntax (
{{ .Values.replicaCount }}), helper functions from_helpers.tpl, and control structures like{{- if .Values.ingress.enabled }}. - Validate locally — run
helm lintto catch YAML syntax errors, thenhelm template .to preview rendered manifests. - Package —
helm package .creates a.tgzarchive with a bundledChart.lockif you have dependencies. - Publish — upload the
.tgzto a Helm repository (e.g., OCI registry viahelm pushor a staticindex.yamlon GitHub Pages, S3, or ChartMuseum).
Hands-on walkthrough
1. Create a minimal chart from scratch
mkdir myapp-chart && cd myapp-chart
cat > Chart.yaml << 'EOF'
apiVersion: v2
name: myapp
version: 0.1.0
appVersion: "1.0.0"
description: A simple NGINX web app
EOF
mkdir templates
cat > templates/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "myapp.name" . }}
template:
metadata:
labels:
app: {{ include "myapp.name" . }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
EOF
cat > values.yaml << 'EOF'
replicaCount: 1
image:
repository: nginx
tag: latest
service:
port: 80
EOF
Pro tip: Use
helm createfor a full scaffold, then delete what you do not need. It saves keystrokes and gives you a standard file layout.
2. Lint and render
helm lint .
# == Results: 0 chart(s) linted, 0 chart(s) failed
helm template .
# ---
# Source: myapp/templates/deployment.yaml
# ---
# apiVersion: apps/v1
# kind: Deployment
# metadata:
# name: release-name-myapp
# ... manifests printed to stdout
The helm template output shows exactly what will be submitted to the cluster — great for debugging.
3. Package the chart
helm package .
# Successfully packaged chart and saved it to: /tmp/myapp-chart/myapp-0.1.0.tgz
4. Publish to an OCI registry (e.g., GitHub Container Registry)
echo $GITHUB_TOKEN | helm registry login ghcr.io -u $GITHUB_USERNAME --password-stdin
helm push myapp-0.1.0.tgz oci://ghcr.io/$GITHUB_USERNAME/helm-charts
# Pushed: ghcr.io/myuser/helm-charts/myapp:0.1.0
Now any authorised user can install directly from the OCI registry:
helm install myapp oci://ghcr.io/myuser/helm-charts/myapp --version 0.1.0
Compare options / when to choose what
| Approach | Best for | Drawback |
|---|---|---|
static index.yaml (GitHub Pages) |
Open‑source, public charts | Manual index update after push |
| OCI registry (GHCR, ECR, ACR) | Teams already using container registries | Requires auth for private repos |
| ChartMuseum | Self‑hosted private repos | Separate server to maintain |
| ArtifactHub | Discovering and sharing public charts | Not a hosting platform (indexes others) |
Mental rule: If your team already has a container registry, use OCI — it is the path Helm 3 natively supports. For fully open-source projects, GitHub Pages with an index is free and frictionless.
Troubleshooting & edge cases
helm lintpasses buthelm installfails – The template references a value that evaluates to an empty string, producing invalid YAML. Usehelm template . | kubectl apply --dry-run=client -f -to let kubectl validate.- Chart contains too many optional resources – Use a
templates/NOTES.txtto print post‑install instructions. Keep_helpers.tplfor reusable named templates. - Dependency chart not found – Run
helm dependency updateto fetch sub‑charts intocharts/and regenerateChart.lock. - Publish to OCI fails with
unauthorized– Ensure you logged in with a token that hasread:packagesandwrite:packagesscopes. - Chart version already exists – Helm enforces semantic versioning. Bump the
versionfield inChart.yamlbefore pushing again.
What you learned & what's next
You now understand how to create and publish a Helm chart — from scaffolding with helm create to packaging and pushing to an OCI registry. You can:
- Parameterise Kubernetes manifests with
values.yaml - Validate chart syntax with
helm lintand preview output withhelm template - Publish a chart as a
.tgzto a registry so anyone can install it withhelm install
The next lesson in this track builds on your publishing skills: managing chart dependencies and upgrading releases — where you will handle multiple sub‑charts and rollback safely.
Practice recap
As a quick exercise, create a chart for a simple NGINX web server with environment‑specific replica counts. Use helm create webapp, customise values.yaml to expose replicaCount and image.tag, then lint and package it. Push the chart to a local ChartMuseum or a free OCI registry like GitHub Container Registry to verify the full publish workflow.
Common mistakes
- Forgetting to bump the
versionfield inChart.yamlbefore repackaging — Helm rejects duplicate package versions. - Using hard-coded values inside templates instead of
{{ .Values.* }}— the chart is not reusable across environments. - Publishing a chart with
helm linterrors — always runhelm lint .andhelm template .first to catch YAML issues early. - Pushing to an OCI registry without prior
helm registry login— you get silent authentication failures.
Variations
- Publish to a static
index.yamlon GitHub Pages instead of an OCI registry — simpler for public open-source charts. - Use
Chart.lockfor reproducible builds with sub‑chart dependencies, thenhelm dependency buildin CI. - Adopt
kubectl krewplugins or Helmfile to orchestrate multiple chart installs — not a direct substitute for publishing, but common in enterprise setups.
Real-world use cases
- Publishing a microservice chart to an internal Helm registry so every team installs the same app version across dev/staging/prod.
- Sharing an open-source monitoring stack (e.g., Prometheus + Grafana) via a public OCI chart on GitHub Container Registry.
- Distributing a vendor app (like a CI/CD runner) as a Helm chart so customers self-install with their own config values.
Key takeaways
- A Helm chart is a packaged directory of Go-templated YAML files plus a
Chart.yamlmanifest. - Scaffold with
helm create, not from scratch — it gives you a standard structure and_helpers.tpl. - Check rendering before deployment:
helm lintfor syntax,helm templatefor output preview. - Package with
helm package .— the.tgzfilename must matchChart.yamlversion. - Publish to an OCI registry with
helm pushfor native auth and versioning, or to a staticindex.yamlfor simplicity. - Always bump the chart version on every change to avoid push conflicts and enable
helm upgrade.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.