Introduction
Deploying a complex application to Kubernetes typically requires creating and managing dozens of YAML manifests: Deployments, Services, ConfigMaps, Secrets, Ingress rules, ServiceAccounts, and more. Managing these files manually is error-prone, tedious, and difficult to reproduce across environments. When you need to deploy the same application to development, staging, and production with slightly different configuration, the problem multiplies.
Helm solves this by providing a package manager for Kubernetes. Just as npm manages JavaScript packages and apt manages Debian packages, Helm manages Kubernetes manifests as "charts"—packaged collections of templated YAML files with configurable values. With Helm, you can install complex applications with a single command, upgrade them safely with automatic rollback on failure, and manage multiple releases of the same application in different namespaces or clusters.
Helm has become the de facto standard for Kubernetes application packaging. The Helm Hub and Artifact Hub host thousands of charts for popular software including PostgreSQL, Redis, NGINX, Prometheus, and virtually every CNCF project. Understanding Helm is essential for any developer or operator working with Kubernetes in production.
Understanding Helm: Core Concepts
Charts, Releases, and Repositories
Helm introduces three core concepts that map to familiar package management patterns:
Chart — A packaged set of Kubernetes manifest templates. A chart is the Helm equivalent of an npm package or a Debian package. It contains templates, default configuration values, metadata, and documentation.
Release — A running instance of a chart in a Kubernetes cluster. When you install a chart, Helm creates a release. You can install the same chart multiple times with different release names and configurations. Each release is independently managed and versioned.
Repository — A collection of charts that can be shared and downloaded. Charts are published to repositories like Artifact Hub, and you can create private repositories for your organization's internal charts.
The Chart Structure
A Helm chart follows a standard directory structure:
mychart/
├── Chart.yaml # Chart metadata (name, version, description)
├── Chart.lock # Dependency lock file
├── values.yaml # Default configuration values
├── values.schema.json # JSON Schema for values validation
├── templates/ # Kubernetes manifest templates
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── serviceaccount.yaml
│ ├── _helpers.tpl # Template helpers (partials)
│ ├── hpa.yaml
│ ├── NOTES.txt # Post-install notes displayed to user
│ └── tests/
│ └── test-connection.yaml
├── charts/ # Dependency charts (sub-charts)
├── crds/ # Custom Resource Definitions
└── README.md # Chart documentation
The Template Engine
Helm uses Go templates to generate Kubernetes manifests from templates and values. The template engine supports conditionals, loops, functions, and pipelines, giving you full control over the generated output.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.targetPort }}Architecture and Design Patterns
Values Override Pattern
The values.yaml file provides default configuration. Users override these defaults during installation or upgrade:
# values.yaml (defaults)
replicaCount: 2
image:
repository: nginx
tag: "1.25"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256MiOverride during installation:
helm install my-release ./mychart \
--set replicaCount=5 \
--set image.tag=1.26 \
--set service.type=LoadBalancerOr use a values file for environment-specific configuration:
helm install my-release ./mychart -f values-production.yaml# values-production.yaml
replicaCount: 10
image:
tag: "1.26"
service:
type: LoadBalancer
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: "2Gi"Helper Templates
The _helpers.tpl file defines reusable template fragments (named templates) that reduce duplication:
# templates/_helpers.tpl
{{/*
Generate chart name and version for chart label.
*/}}
{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Fullname
*/}}
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}Dependency Management
Charts can depend on other charts, specified in Chart.yaml:
# Chart.yaml
apiVersion: v2
name: myapp
description: My application with PostgreSQL
type: application
version: 1.0.0
appVersion: "2.0"
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "17.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled# Update dependencies
helm dependency update ./mychart
# This creates Chart.lock and downloads charts to charts/Step-by-Step Implementation
Creating a New Chart
# Create a new chart from the default template
helm create myapp
# The generated structure includes:
# - Deployment, Service, Ingress, HPA, ServiceAccount templates
# - Default values.yaml
# - Helper templates in _helpers.tpl
# - Test templates
# Customize the chart
cd myappBuilding a Complete Application Chart
Here is a complete chart for a Node.js API application:
# Chart.yaml
apiVersion: v2
name: node-api
description: A Node.js REST API with PostgreSQL
type: application
version: 0.1.0
appVersion: "1.0.0"# values.yaml
replicaCount: 2
image:
repository: myregistry.io/node-api
tag: "1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 3000
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.example.com
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 60
env:
NODE_ENV: production
LOG_LEVEL: info
secrets:
DATABASE_URL: ""
API_KEY: ""# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "node-api.fullname" . }}
labels:
{{- include "node-api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "node-api.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "node-api.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "node-api.fullname" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
envFrom:
- configMapRef:
name: {{ include "node-api.fullname" . }}
{{- if .Values.secrets.DATABASE_URL }}
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "node-api.fullname" . }}
key: DATABASE_URL
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 20Installing, Upgrading, and Rolling Back
# Install a release
helm install my-api ./node-api \
--namespace production \
--create-namespace \
--set secrets.DATABASE_URL=postgres://user:pass@host/db
# Check release status
helm status my-api -n production
# Upgrade with new values
helm upgrade my-api ./node-api \
--namespace production \
--set image.tag=2.0.0 \
--set replicaCount=5
# View release history
helm history my-api -n production
# Rollback to previous revision
helm rollback my-api 1 -n production
# Uninstall
helm uninstall my-api -n productionReal-World Use Cases
Deploying a Full Application Stack
Deploy an application with its database, cache, and monitoring:
# Add repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# Install PostgreSQL
helm install my-postgres bitnami/postgresql \
--set auth.database=myapp \
--set auth.username=myuser \
--set auth.existingSecret=my-postgres-secret
# Install Redis
helm install my-redis bitnami/redis \
--set architecture=standalone \
--set auth.existingSecret=my-redis-secret
# Install the application
helm install my-api ./node-api \
--set secrets.DATABASE_URL="postgres://myuser:pass@my-postgres/myapp"
# Install Prometheus monitoring
helm install monitoring prometheus-community/kube-prometheus-stackMulti-Environment Deployment with Kustomize + Helm
For teams that prefer Kustomize for overlay management but want Helm charts for third-party software:
# kustomization.yaml
helmCharts:
- name: postgresql
repo: https://charts.bitnami.com/bitnami
version: 12.x.x
releaseName: my-postgres
namespace: database
values:
auth:
database: myappChart Packaging and Distribution
# Lint the chart for errors
helm lint ./mychart
# Render templates locally without installing
helm template my-release ./mychart -f values.yaml
# Package the chart for distribution
helm package ./mychart
# Creates mychart-0.1.0.tgz
# Push to a chart repository (OCI-based)
helm push mychart-0.1.0.tgz oci://myregistry.io/helm-charts
# Install from OCI repository
helm install my-release oci://myregistry.io/helm-charts/mychart --version 0.1.0Best Practices for Production
-
Always use
helm templatebeforehelm install— Render the templates locally to verify the generated YAML is correct before applying to the cluster. -
Use Chart.yaml versioning semantically — Follow SemVer for chart versions. Increment the patch version for bug fixes, minor for backward-compatible features, major for breaking changes.
-
Pin dependency versions — Use exact versions or version ranges in
Chart.yamldependencies. UseChart.lockto ensure reproducible builds. -
Use
--atomicfor production upgrades — The--atomicflag automatically rolls back if the upgrade fails, preventing partially deployed releases. -
Set
--timeoutfor long-running deployments — Default timeout is 5 minutes. Increase it for large deployments or slow-starting applications. -
Use
values.schema.jsonfor validation — Define a JSON Schema for your values to catch configuration errors duringhelm installrather than at runtime. -
Store chart values in version control — Maintain separate values files for each environment (values-dev.yaml, values-staging.yaml, values-prod.yaml) in your Git repository.
-
Use Helmfile or Helmsman for multi-chart deployments — Tools like Helmfile provide declarative management of multiple Helm releases, enabling you to deploy an entire stack with a single command.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Hardcoded values in templates | Chart not reusable across environments | Use {{ .Values.xxx }} for all configurable values |
| No resource requests/limits | Unpredictable scheduling and OOMKills | Always define resources in values.yaml with sensible defaults |
Missing NOTES.txt | Users don't know how to access the deployed app | Add NOTES.txt with connection instructions |
Not using helm lint | Template errors caught during installation | Lint before every install and upgrade |
| Secrets in values.yaml | Secrets committed to Git | Use external secret managers or --set with CI/CD secret injection |
| No rollback strategy | Failed upgrades require manual recovery | Use --atomic flag for automatic rollback |
| Large monolithic charts | Difficult to maintain and test | Split into smaller charts with dependencies |
Ignoring helm diff | Unexpected changes during upgrade | Use helm-diff plugin to preview changes before upgrading |
Performance Optimization
# Install helm-diff plugin to preview changes
helm plugin install https://github.com/databus23/helm-diff
# Preview changes before upgrading
helm diff upgrade my-api ./node-api -f values-prod.yaml
# Use --reuse-values to only change specific values during upgrade
helm upgrade my-api ./node-api --reuse-values --set replicaCount=10
# Use helmfile for parallel deployments
helmfile apply --concurrency 4For large clusters with many releases, use Helm's release history limits to prevent storage bloat:
# Limit history to 10 revisions
helm upgrade my-api ./node-api --history-max 10Comparison with Alternatives
| Feature | Helm | Kustomize | Jsonnet/Tanka | Carvel ytt | ArgoCD |
|---|---|---|---|---|---|
| Templating | Go templates | Overlays | Jsonnet | YAML-native | N/A (GitOps) |
| Package format | Chart (.tgz) | Directory | Bundle | Directory | N/A |
| Dependency management | Built-in | None | Jsonnet Bundler | None | Git-based |
| Rollback | Built-in | Manual | Manual | Manual | Built-in |
| Release management | Yes | No | No | No | Yes (GitOps) |
| Learning curve | Medium | Low | High | Low | Medium |
| Community charts | Thousands | Few | Few | Few | N/A |
| Best for | Third-party apps | Custom overlays | Complex configs | YAML-native config | GitOps delivery |
Helm is the best choice for deploying third-party software and managing releases. Kustomize is better for customizing base manifests without templating. Many teams use both: Helm for third-party charts, Kustomize for in-house applications.
Advanced Patterns
Chart Hooks for Lifecycle Events
Helm hooks run scripts at specific points in the release lifecycle:
# templates/job-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "mychart.fullname" . }}-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["node", "scripts/migrate.js"]
restartPolicy: NeverNested Charts (Sub-charts)
Create parent-child chart relationships for modular applications:
# Parent Chart.yaml
dependencies:
- name: frontend
version: "1.0.0"
repository: "file://../frontend-chart"
- name: api
version: "2.0.0"
repository: "file://../api-chart"# Override sub-chart values from parent values.yaml
frontend:
replicaCount: 3
ingress:
enabled: true
api:
replicaCount: 5
autoscaling:
enabled: trueLookup Function for Dynamic Configuration
Use the lookup function to read existing cluster resources:
{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
apiVersion: v1
kind: Secret
metadata:
name: my-secret
data:
# Preserve existing password if secret already exists
password: {{ index $secret.data "password" }}
{{- else }}
apiVersion: v1
kind: Secret
metadata:
name: my-secret
data:
password: {{ randAlphaNum 24 | b64enc }}
{{- end }}Testing Strategies
# Lint chart for errors
helm lint ./mychart
# Render templates and check output
helm template test-release ./mychart -f values.yaml > rendered.yaml
kubectl apply --dry-run=client -f rendered.yaml
# Run chart tests
helm test my-release -n production
# Use helm-unittest for unit testing templates
helm plugin install https://github.com/helm-unittest/helm-unittest
helm unittest ./mychart# tests/deployment_test.yaml
suite: deployment
templates:
- deployment.yaml
tests:
- it: should set replica count
set:
replicaCount: 5
asserts:
- isKind:
of: Deployment
- equal:
path: spec.replicas
value: 5
- it: should use custom image tag
set:
image.tag: "2.0"
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: "nginx:2.0"Future Outlook
Helm continues to be the dominant Kubernetes package manager. Helm 4 (currently in development) promises improved dependency management, better OCI registry integration, and enhanced security features. The Helm project has also been working on improved support for Kubernetes Custom Resources and better integration with GitOps workflows.
The OCI-based chart distribution model is replacing traditional chart repositories, enabling organizations to store charts alongside container images in their existing registry infrastructure. This simplifies the supply chain and enables unified access control and vulnerability scanning.
Conclusion
Helm is the standard package manager for Kubernetes, providing templating, release management, dependency handling, and chart distribution. It transforms complex multi-file Kubernetes deployments into manageable, versioned, and reproducible packages.
Key takeaways:
- Helm charts package Kubernetes manifests as templated, configurable units
- Releases are running instances of charts with independent lifecycle management
- Values files provide environment-specific configuration without modifying templates
- Helper templates (
_helpers.tpl) reduce duplication and ensure consistent labeling - Always lint charts and render templates before installing to catch errors early
- Use
--atomicfor production upgrades to enable automatic rollback on failure - Store chart values in version control and use CI/CD for deployment
- Use helm-diff to preview changes before upgrading production releases
Start by creating a chart for your application with helm create, customize the templates and values, and progressively add features like autoscaling, ingress, and health probes. For deeper exploration, see the Helm documentation, the Helm Chart Best Practices, and the Artifact Hub for discovering community charts.