Somewhere around my fifteenth YAML file for a single application — Deployment, Service, ConfigMap, Secret, Ingress, HPA — I realized I needed a better way to manage all of it. That’s what led me to Helm, which I now think of as the closest thing Kubernetes has to a proper package manager. In this guide, I’ll cover what Helm actually is, how charts are structured, and walk through installing, customizing, and even authoring your own chart.
What Is Helm and Why Use It?
Helm packages a set of Kubernetes manifests into a single unit called a chart. Instead of applying a dozen YAML files by hand, you run one command to install, upgrade, or roll back an entire application, complete with templated values so the same chart can be reused across dev, staging, and production with different configuration.
The core concepts:
- Chart — a package of templated Kubernetes manifests plus metadata.
- Release — a specific instance of a chart deployed into a cluster.
- Values — the configuration passed into a chart’s templates, usually via a
values.yamlfile. - Repository — a collection of charts you can install from, similar to an apt or npm repo.
Step 1: Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
Output:
version.BuildInfo{Version:"v3.15.0", GitCommit:"...", GoVersion:"go1.22.1"}
Step 2: Add a Chart Repository and Install Something Real
Let’s install Prometheus using its official chart, a common real-world task:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community
NAME CHART VERSION APP VERSION
prometheus-community/prometheus 25.24.0 2.53.0
prometheus-community/kube-prometheus... 61.3.2 0.75.0
Install it into its own namespace:
helm install prometheus prometheus-community/prometheus \
--namespace monitoring \
--create-namespace
Check the release:
helm list -n monitoring
NAME NAMESPACE REVISION STATUS CHART APP VERSION
prometheus monitoring 1 deployed prometheus-25.24.0 2.53.0
Step 3: Customize Values Instead of Editing Templates Directly
Every chart ships a values.yaml with sensible defaults. Rather than hand-editing rendered manifests, you override values:
helm show values prometheus-community/prometheus > my-values.yaml
Edit the parts you care about, for example bumping retention and storage size:
# my-values.yaml (partial)
server:
retention: "30d"
persistentVolume:
size: 50Gi
alertmanager:
enabled: true
Apply with the override:
helm upgrade prometheus prometheus-community/prometheus \
-n monitoring \
-f my-values.yaml
You can also override individual values inline without a file:
helm upgrade prometheus prometheus-community/prometheus \
-n monitoring \
--set server.retention=30d \
--set server.persistentVolume.size=50Gi
Step 4: Rolling Back a Release
If an upgrade introduces a problem, Helm keeps revision history so rollback is trivial:
helm history prometheus -n monitoring
REVISION UPDATED STATUS CHART DESCRIPTION
1 Mon Jul 28 10:02:11 2026 superseded prometheus-25.24.0 Install complete
2 Mon Jul 28 11:15:44 2026 deployed prometheus-25.24.0 Upgrade complete
helm rollback prometheus 1 -n monitoring
This restores the exact manifests from revision 1 — no manual YAML archaeology required.
Step 5: Writing Your Own Chart
For your own applications, scaffold a new chart:
helm create myapp
This generates a standard structure:
myapp/
Chart.yaml
values.yaml
templates/
deployment.yaml
service.yaml
ingress.yaml
_helpers.tpl
tests/
charts/
Chart.yaml holds metadata:
apiVersion: v2
name: myapp
description: A Helm chart for myapp
type: application
version: 0.1.0
appVersion: "1.0.0"
A trimmed templates/deployment.yaml using Go templating to pull from values.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-myapp
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}-myapp
template:
metadata:
labels:
app: {{ .Release.Name }}-myapp
spec:
containers:
- name: myapp
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
And the corresponding values.yaml:
replicaCount: 2
image:
repository: myrepo/myapp
tag: "1.0.0"
service:
port: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
Render it locally to check the output before installing (extremely useful for catching template mistakes):
helm template myapp ./myapp
Lint the chart for common mistakes:
helm lint ./myapp
Then install:
helm install myapp-dev ./myapp --namespace dev --create-namespace
Step 6: Managing Secrets Safely
Never put real secrets directly in values.yaml committed to git. Instead, either pass them at install time:
helm install myapp ./myapp --set secrets.dbPassword=$DB_PASSWORD
Or better, integrate with a secrets manager like Sealed Secrets, External Secrets Operator, or Vault, and reference the resulting Kubernetes Secret from your templates rather than baking credentials into values files at all.
Step 7: Helm in CI/CD
A typical GitOps-friendly upgrade step in a pipeline:
helm upgrade --install myapp ./charts/myapp \
--namespace production \
--set image.tag=$CI_COMMIT_SHA \
--wait \
--timeout 5m \
--atomic
The --atomic flag is important — it automatically rolls back if the upgrade fails, and --wait blocks until Pods are actually ready, giving your pipeline a reliable pass/fail signal instead of a false positive.
Debugging Helm Issues
helm get manifest prometheus -n monitoring # see exactly what was applied
helm get values prometheus -n monitoring # see the values actually used
helm status prometheus -n monitoring # current release status
If a release gets stuck in a pending-upgrade state after a failed deploy:
helm rollback prometheus 0 -n monitoring
Best Practices
- Pin chart versions explicitly in production (
--version 25.24.0) rather than always pulling latest. - Keep environment-specific values in separate files (
values-dev.yaml,values-prod.yaml) layered with-f. - Use
helm diff(a plugin) before upgrading production to preview exactly what will change. - Store charts for your own apps in a proper OCI registry or chart repo, versioned alongside your app releases.
- Avoid excessive templating logic in charts — if a template needs heavy conditionals for every edge case, it may be a sign to split into multiple charts.
Common Mistakes
- Forgetting
--create-namespaceand having installs silently fail against a nonexistent namespace. - Not linting charts before install, missing indentation errors in templates.
- Treating
values.yamldefaults as production-ready without review — many community charts default to minimal resource requests unsuitable for real workloads.
Chart Dependencies and Subcharts
Real applications often need more than one chart working together — your app plus a Redis cache plus a Postgres instance, for example. Helm supports this through dependencies declared in Chart.yaml:
apiVersion: v2
name: myapp
version: 0.1.0
dependencies:
- name: redis
version: "19.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
helm dependency update ./myapp
This pulls the dependent chart into myapp/charts/, and its values become configurable as a nested key in your own values.yaml:
redis:
enabled: true
auth:
password: "changeme"
This pattern lets you compose complex, multi-service applications from a mix of your own charts and well-maintained community charts, without duplicating their internal logic.
Helm Hooks for Lifecycle Management
Sometimes you need something to run at a specific point in a release’s lifecycle — a database migration before the main Deployment updates, for instance. Helm hooks handle exactly this:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate.sh"]
restartPolicy: Never
The pre-upgrade,pre-install annotation ensures this Job runs and completes successfully before Helm proceeds with updating the main application resources — giving you a reliable way to sequence schema migrations ahead of a code deploy within a single helm upgrade invocation.
Testing Charts Before They Reach Production
Beyond helm lint and helm template, Helm supports test hooks that validate a release actually works after install:
apiVersion: v1
kind: Pod
metadata:
name: {{ .Release.Name }}-test-connection
annotations:
"helm.sh/hook": test
spec:
containers:
- name: test
image: curlimages/curl
command: ["curl", "-f", "http://{{ .Release.Name }}-myapp:8080/healthz"]
restartPolicy: Never
helm test myapp-dev -n dev
This spins up the test Pod, checks its exit code, and reports pass/fail — a nice automated smoke test you can wire directly into a CI/CD pipeline right after helm upgrade --install.
Working with Private Chart Repositories
For internal charts, storing them in a plain HTTP chart repo or, more commonly today, an OCI-compliant registry (many teams reuse their existing container registry) keeps versioning and distribution consistent with the rest of your artifacts:
helm package ./myapp
helm push myapp-0.1.0.tgz oci://myregistry.example.com/helm-charts
helm install myapp oci://myregistry.example.com/helm-charts/myapp --version 0.1.0
This OCI-based flow has become the more common pattern in recent Helm versions, since it avoids standing up and maintaining a separate chart-repository index.
Common Helm Anti-Patterns to Avoid
- Templating so much conditional logic into one chart that it becomes unreadable — if you find yourself writing deeply nested
{{- if }}blocks to support wildly different deployment shapes, consider splitting into separate charts instead. - Committing rendered Secrets (even base64-encoded) into a values file tracked in git — base64 is encoding, not encryption, and anyone with repo access can trivially decode it.
- Relying on
latestas a chart version in CI/CD, which makes deployments non-reproducible and rollbacks unreliable. - Skipping
helm lintandhelm templatein CI, catching template errors only at actual deploy time in a real environment.
Summary
Helm turns the sprawl of raw Kubernetes YAML into versioned, reusable, configurable packages. Whether you’re installing complex third-party software like Prometheus or packaging your own microservices, the workflow is the same: install or upgrade a chart, override values for your environment, and roll back instantly if something goes wrong. Once you’re comfortable with charts, templates, and values, Helm becomes the backbone of a much more maintainable Kubernetes deployment process.