Guide
Helm Kubernetes explained
Helm is the de facto package manager for Kubernetes. Instead of
hand-applying dozens of YAML files per environment, you bundle manifests into a
versioned chart, parameterize them with values, and
install releases that Helm tracks for upgrade and rollback. Whether
you are deploying the official ingress-nginx chart, packaging your own
microservice stack, or wiring a
CI/CD pipeline
to promote staging values into production, Helm sits between raw kubectl and full
GitOps controllers. This guide
covers Helm 3 architecture (no cluster-side Tiller), chart anatomy and Go templating,
values layering, repositories, lifecycle commands, hooks and dependencies, a Harbor
Fleet multi-service worked example, a Helm vs Kustomize vs operators decision table,
common pitfalls, and a production checklist. For cluster primitives Helm renders into,
start with
Kubernetes fundamentals.
What problem Helm solves
A typical production service on Kubernetes needs a Deployment, Service, Ingress, ConfigMap, Secret references, HorizontalPodAutoscaler, PodDisruptionBudget, and maybe a ServiceMonitor for Prometheus. Copy-pasting YAML across dev, staging, and prod invites drift: one environment forgets a label selector, another hard-codes an image tag. Helm centralizes those manifests in a chart and injects environment-specific values at render time.
Helm also records release history. An helm upgrade creates a new
revision; helm rollback re-applies a prior revision without hunting
through git. That operational memory is why platform teams reach for Helm before
they adopt Argo CD or Flux — though GitOps tools often use Helm charts
under the hood.
Helm 3 vs the old Tiller model
Helm 2 ran a cluster-admin Tiller pod that applied resources — a security liability on multi-tenant clusters. Helm 3 removed Tiller; the CLI talks directly to the Kubernetes API using your kubeconfig credentials. Release metadata lives in Secrets (or ConfigMaps) in the release namespace. If you encounter Tiller references in old blog posts, ignore them.
Charts, templates, and values
A chart is a directory (or packaged .tgz) with a
predictable layout:
Chart.yaml— name, version, appVersion, dependencies.values.yaml— default parameters consumers override.templates/— Kubernetes manifests with Go-template placeholders.templates/_helpers.tpl— reusable named templates (labels, names).templates/NOTES.txt— post-install hints shown to the operator.
Templates use expressions like {{ .Values.replicaCount }} and control
flow (if, range, with). Helm renders them
into plain YAML, then applies the result. The helm template command
prints rendered manifests without touching the cluster — essential for code review
and CI validation.
Values layering
Override defaults in order of precedence (lowest to highest): chart
values.yaml, parent chart values, user-supplied
-f my-values.yaml files, and --set key=value flags.
Production patterns keep a values-base.yaml plus
values-staging.yaml / values-prod.yaml that only differ
on image tags, replica counts, and ingress hosts. Never commit secrets in values
files checked into git — inject them via
secrets management
(External Secrets Operator, Sealed Secrets, or CI-injected --set).
Releases and revisions
Installing my-api from chart harbor-api-1.4.2.tgz creates
a release named my-api at revision 1.
Each upgrade bumps the revision while keeping the release name. List history with
helm history my-api; rollback with
helm rollback my-api 3. Uninstall removes resources defined in the
chart (unless annotated to keep resources on delete).
Repositories, dependencies, and hooks
Chart repositories are HTTP servers (or OCI registries) hosting
packaged charts. Add one with helm repo add bitnami https://charts.bitnami.com/bitnami,
then helm search repo nginx. The Artifact Hub index catalogs thousands
of community charts — vet them for maintenance and security before production use.
Subcharts and dependencies
Declare dependencies in Chart.yaml under dependencies:.
Running helm dependency update vendors subcharts into
charts/. A common pattern: your application chart depends on the
official postgresql or redis subchart for local dev,
while production points values at a managed database instead. Pass values to
subcharts with a key matching the dependency name:
postgresql.auth.password: ....
Lifecycle hooks
Annotate resources with helm.sh/hook: pre-install,post-upgrade (among
others) to run Jobs before or after the main manifest apply — database migrations,
cache warming, smoke tests. Hooks run in weighted order; failed hooks abort the
release. Use hooks sparingly: they complicate rollback and GitOps reconciliation.
Prefer running migrations from CI or a dedicated Job outside Helm when possible.
Worked example: Harbor Fleet platform chart
Harbor Fleet runs five stateless APIs (payments, catalog, support, courier, analytics)
behind a shared ingress controller on one Kubernetes cluster. Without Helm, each
service duplicated Deployment/Service/Ingress YAML with copy-paste labels. The
platform team built an harbor-service chart parameterized by:
image.repository/image.tag— per-service container.ingress.host—payments.harbor.internaletc.resources— CPU/memory requests tuned per workload.autoscaling.enabledandautoscaling.maxReplicas— ties into HPA templates.serviceMonitor.enabled— optional Prometheus scrape config.
They install five releases from the same chart version with different values files:
helm upgrade --install payments ./harbor-service \
-f values-base.yaml -f values-payments.yaml \
--namespace harbor --create-namespace
helm upgrade --install catalog ./harbor-service \
-f values-base.yaml -f values-catalog.yaml \
--namespace harbor
A platform harbor-platform umbrella chart declares dependencies on
ingress-nginx, cert-manager, and harbor-service
(aliased five times). One helm upgrade on the umbrella upgrades shared
infra and all apps with locked dependency versions. CI runs
helm lint and helm template on every pull request; CD
promotes the same chart tarball from staging to prod, changing only
values-prod.yaml image tags after integration tests pass.
Helm vs alternatives — decision table
| Approach | Best for | Trade-offs |
|---|---|---|
| Raw YAML + kubectl | One-off demos, learning K8s primitives | No parameterization, no release history, environment drift |
| Helm charts | Packaged apps, third-party installs, multi-env parameterization | Go-template complexity, hook edge cases, chart versioning discipline |
| Kustomize overlays | Patches on base manifests without templating language | Weaker packaging/distribution story; no built-in release rollback |
| GitOps (Argo CD / Flux) | Continuous reconcile from git, audit trail, multi-cluster | Operational overhead; often wraps Helm or Kustomize anyway |
| Operators / CRDs | Stateful systems needing domain-specific lifecycle (databases, Kafka) | High build cost; overkill for stateless HTTP services |
Many teams use Helm for packaging and GitOps for delivery: chart version bumps merge to git; Argo CD syncs the rendered release. That split keeps templating in Helm and auditability in git.
Common pitfalls
- Secrets in values.yaml committed to git — use External Secrets or CI-injected overrides; rotate anything ever leaked.
- Non-deterministic templates —
randAlphaNumin templates creates new resources every upgrade; set explicit names. - Hook Jobs that never complete — stuck pre-upgrade hooks block all future upgrades until manually deleted.
- Chart version vs app version confusion — bump
versionin Chart.yaml on every chart change;appVersiontracks the container image. - Blind
helm upgrade --force— recreates resources and can cause brief outages; understand before using. - Untested subchart upgrades — dependency version bumps can change default security contexts or labels; diff rendered YAML in CI.
- Cluster-scoped resources in app charts — ClusterRoles and CRDs belong in platform charts installed once, not per microservice release.
Production checklist
- Pin chart and dependency versions; avoid floating
latesttags in production values. - Run
helm lintandhelm templatein CI on every chart change. - Store environment values in git (minus secrets); tag releases that match deployed revisions.
- Document rollback runbooks:
helm history,helm rollback, and when to fix-forward instead. - Use
--atomicor--waitin CD for critical services so failed upgrades auto-rollback. - Namespace releases per team or environment; restrict RBAC so CI service accounts cannot helm-install cluster-wide.
- Sign charts (Helm provenance) or pull from trusted OCI registries with digest pinning.
- Keep NOTES.txt accurate — onboarding engineers should know the first kubectl command after install.
- Align chart labels with your observability stack (standard
app.kubernetes.io/*labels). - Rehearse disaster recovery: reinstall from chart + values, not from memory.
Key takeaways
- Helm packages Kubernetes manifests into versioned charts with templated values and tracked release history.
- Helm 3 applies directly via the API — no Tiller — with rollback via numbered revisions.
- Layer values files per environment; never commit plaintext secrets.
- Subcharts and repositories distribute reusable platform and application packages.
- Helm pairs naturally with GitOps and CI/CD; it complements rather than replaces Kustomize or operators.
Related reading
- Kubernetes fundamentals explained — pods, Deployments, Services, and Ingress
- GitOps explained — Argo CD and Flux delivering Helm charts from git
- Docker fundamentals explained — images and containers Helm deploys
- CI/CD pipelines explained — linting, testing, and promoting chart releases