Managing Kubernetes Applications with Helm
A practical guide to Helm charts, releases, and values - packaging, templating, and upgrading Kubernetes applications
Introduction
Helm is the standard package manager for Kubernetes: a chart bundles a set of manifests as reusable, versioned, parameterized templates, and a release is one deployed instance of a chart with a specific set of values. This guide covers chart structure, installing and upgrading releases, and writing your own chart.
Installing Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
Working with Existing Charts
# Add a chart repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Search for a chart
helm search repo bitnami/postgresql
# Inspect a chart's default values before installing
helm show values bitnami/postgresql > postgresql-values.yaml
# Install a release
helm install my-db bitnami/postgresql \
--namespace data \
--create-namespace \
--values postgresql-values.yaml \
--set auth.database=appdb
# List releases
helm list --namespace data
# Upgrade a release with new values
helm upgrade my-db bitnami/postgresql \
--namespace data \
--reuse-values \
--set primary.persistence.size=50Gi
# Roll back to the previous revision
helm rollback my-db --namespace data
# Uninstall
helm uninstall my-db --namespace data
--reuse-values on upgrade merges your new --set/--values on top of the release’s currently deployed values rather than resetting to the chart’s defaults - without it, an upgrade that only changes one setting silently reverts every other value you’d previously customized.
Chart Structure
my-app/
├── Chart.yaml # Chart metadata: name, version, appVersion
├── values.yaml # Default configuration values
├── values.schema.json # Optional: JSON Schema validating values
├── charts/ # Bundled dependency charts
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # Reusable template snippets
│ └── NOTES.txt # Printed after install/upgrade
└── .helmignore
# Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my-app
type: application
version: 0.1.0 # Chart version - bump on every change to the chart itself
appVersion: "1.4.2" # Version of the application the chart deploys
version and appVersion track two different things and change independently: version is the chart’s own release number (bump it whenever templates or defaults change), appVersion is just informational metadata about which version of your application image the chart currently points at.
Writing a Template
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
# values.yaml
replicaCount: 2
image:
repository: myregistry/my-app
tag: "" # defaults to .Chart.AppVersion when unset
service:
port: 8080
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
{{ include "my-app.fullname" . }} calls a named template usually defined in _helpers.tpl - the convention (generated by helm create) computes a consistent <release-name>-<chart-name> naming scheme so every resource in the chart is named predictably without repeating the logic in every template file.
Testing and Validating
# Render templates locally without installing anything
helm template my-app ./my-app --values values.yaml
# Validate against the Kubernetes API without persisting (server-side dry run)
helm install my-app ./my-app --dry-run --debug
# Lint the chart for common mistakes
helm lint ./my-app
helm template is the fastest feedback loop for template logic - it never touches a cluster - while --dry-run on helm install/upgrade additionally validates the rendered manifests against the live API server’s schema (catching things like an invalid resources value that helm template alone wouldn’t).
Dependencies
# Chart.yaml
dependencies:
- name: postgresql
version: "14.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
helm dependency update ./my-app
condition: postgresql.enabled lets consumers of your chart opt out of the bundled PostgreSQL dependency entirely via --set postgresql.enabled=false, useful when they already run a database elsewhere and only want your application chart’s own resources.
Best Practices
- Pin chart versions explicitly (
helm install --version 14.2.1, not the latest floating version) in anything beyond local experimentation - an unpinned chart install can pull in breaking changes between when you tested and when a pipeline actually runs it. - Use
--atomicon upgrades in CI/CD - it automatically rolls back to the previous release if the upgrade fails, rather than leaving the release in a partially-applied state. - Keep secrets out of
values.yamlcommitted to version control - reference an existing Kubernetes Secret, or use a tool likehelm-secretsor the External Secrets Operator instead of plaintext values. - Use
values.schema.jsonto validate required values and their types at install time, catching a misconfigured--setbefore it reaches the API server rather than after.
Conclusion
Helm’s chart/release/values model solves the same “reusable, parameterized manifests” problem Kustomize solves with overlays and patches, but takes a templating approach instead - useful when the same chart needs to serve many different consumers with very different configuration needs, at the cost of the string-templating footguns (accidental YAML indentation errors, values that silently don’t apply) that a patch-based tool avoids.
Additional Resources
- Helm Documentation
- Artifact Hub - search public chart repositories
- Helm Best Practices Guide