Helm is the package manager for Kubernetes, simplifying deployment and management of complex applications. This comprehensive guide covers everything from basic concepts to advanced chart development and best practices.
Why Helm?
Helm solves several Kubernetes deployment challenges:
- Templating: Dynamic YAML generation with values
- Versioning: Track and rollback releases
- Dependency Management: Manage chart dependencies
- Reusability: Share and reuse configurations
- Simplified Operations: Install/upgrade/rollback with single commands
Installation
# Install via script
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Install on macOS
brew install helm
# Install on Ubuntu/Debian
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm
# Verify installation
helm version
# Add shell completion
helm completion bash >> ~/.bashrc
source ~/.bashrc
Getting Started
# Add a repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Search for charts
helm search repo nginx
helm search hub nginx
# Install a chart
helm install my-nginx bitnami/nginx
# List releases
helm list
helm list --all-namespaces
# Get release info
helm get all my-nginx
helm get values my-nginx
helm get manifest my-nginx
# Upgrade release
helm upgrade my-nginx bitnami/nginx --set replicaCount=3
# Rollback release
helm rollback my-nginx 1
# Uninstall release
helm uninstall my-nginx
Creating Your First Chart
# Create chart
helm create myapp
# Directory structure
myapp/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── charts/ # Chart dependencies
├── templates/ # Template files
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # Template helpers
│ ├── NOTES.txt # Post-install notes
│ └── tests/ # Test files
└── .helmignore # Files to ignore
Chart.yaml
# Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for my application
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- web
- application
- nodejs
maintainers:
- name: DevOps Team
email: devops@example.com
url: https://example.com
dependencies:
- name: postgresql
version: "12.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: "17.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
values.yaml
# values.yaml
replicaCount: 3
image:
repository: myregistry.com/myapp
pullPolicy: IfNotPresent
tag: "1.0.0"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
service:
type: ClusterIP
port: 80
targetPort: 8080
annotations: {}
ingress:
enabled: false
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- myapp.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Application-specific values
config:
logLevel: info
database:
host: postgresql
port: 5432
name: myapp
redis:
host: redis
port: 6379
postgresql:
enabled: true
auth:
database: myapp
username: myapp
primary:
persistence:
size: 10Gi
redis:
enabled: true
architecture: standalone
master:
persistence:
size: 5Gi
Deployment Template
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
env:
- name: LOG_LEVEL
value: {{ .Values.config.logLevel | quote }}
- name: DB_HOST
value: {{ .Values.config.database.host | quote }}
- name: DB_PORT
value: {{ .Values.config.database.port | quote }}
- name: DB_NAME
value: {{ .Values.config.database.name | quote }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}-db
key: password
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 10
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: config
configMap:
name: {{ include "myapp.fullname" . }}
- name: tmp
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
Helper Templates
# templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
Chart Management
Install and Upgrade
# Install with custom values
helm install myapp ./myapp \
--namespace production \
--create-namespace \
--values values-production.yaml \
--set image.tag=1.2.0
# Dry run
helm install myapp ./myapp --dry-run --debug
# Upgrade
helm upgrade myapp ./myapp \
--values values-production.yaml \
--set replicaCount=5
# Install or upgrade
helm upgrade --install myapp ./myapp \
--values values-production.yaml
# Wait for resources to be ready
helm upgrade myapp ./myapp --wait --timeout 5m
# Force recreation
helm upgrade myapp ./myapp --force
# Atomic upgrade (rollback on failure)
helm upgrade myapp ./myapp --atomic --timeout 10m
Multiple Values Files
# values-dev.yaml
replicaCount: 1
ingress:
hosts:
- host: dev.example.com
config:
logLevel: debug
postgresql:
enabled: true
---
# values-staging.yaml
replicaCount: 2
ingress:
hosts:
- host: staging.example.com
config:
logLevel: info
---
# values-production.yaml
replicaCount: 5
ingress:
enabled: true
hosts:
- host: example.com
config:
logLevel: warn
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
# Install with multiple values files
helm install myapp ./myapp \
-f values.yaml \
-f values-production.yaml \
--set image.tag=$(git rev-parse --short HEAD)
Advanced Patterns
Conditionals
# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
Loops
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}
data:
{{- range $key, $value := .Values.config }}
{{ $key }}: {{ $value | quote }}
{{- end }}
Named Templates with Arguments
# templates/_helpers.tpl
{{- define "myapp.envVars" -}}
{{- range $key, $value := . }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
# Usage in deployment
env:
{{- include "myapp.envVars" .Values.env | nindent 2 }}
Dependencies
# Chart.yaml
dependencies:
- name: postgresql
version: "~12.0.0"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: "^17.0.0"
repository: "@bitnami"
condition: redis.enabled
alias: cache
# Update dependencies
helm dependency update
# List dependencies
helm dependency list
# Build dependencies
helm dependency build
Hooks
# templates/job-migration.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-migration
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
metadata:
name: {{ include "myapp.fullname" . }}-migration
spec:
restartPolicy: Never
containers:
- name: migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["npm", "run", "migrate"]
Hook types:
pre-install
: Before resources are installedpost-install
: After all resources are installedpre-delete
: Before any resources are deletedpost-delete
: After all resources are deletedpre-upgrade
: Before resources are upgradedpost-upgrade
: After all resources are upgradedpre-rollback
: Before resources are rolled backpost-rollback
: After resources are rolled back
Testing
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "myapp.fullname" . }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
# Run tests
helm test myapp
# Run tests with logs
helm test myapp --logs
Helmfile
Manage multiple Helm releases:
# helmfile.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
helmDefaults:
wait: true
timeout: 600
recreatePods: false
force: false
releases:
- name: postgresql
namespace: databases
chart: bitnami/postgresql
version: ~12.0.0
values:
- values/postgresql.yaml
secrets:
- secrets/postgresql-secrets.yaml
- name: myapp
namespace: production
chart: ./charts/myapp
values:
- values/myapp-production.yaml
set:
- name: image.tag
value: {{ requiredEnv "IMAGE_TAG" }}
needs:
- databases/postgresql
- name: prometheus
namespace: monitoring
chart: prometheus-community/kube-prometheus-stack
version: ~45.0.0
values:
- values/prometheus.yaml
# Install helmfile
brew install helmfile
# Sync all releases
helmfile sync
# Apply specific release
helmfile -l name=myapp sync
# Diff changes
helmfile diff
# Destroy all releases
helmfile destroy
Chart Repository
Create Chart Repository
# Package chart
helm package myapp
# Create index
helm repo index . --url https://charts.example.com
# Upload to GitHub Pages
git add .
git commit -m "Add chart"
git push origin gh-pages
Host on GitHub Pages
# .github/workflows/release.yml
name: Release Charts
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Install Helm
uses: azure/setup-helm@v3
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.5.0
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
Best Practices
✅ Use specific chart versions
✅ Document values in values.yaml
✅ Use _helpers.tpl for reusable templates
✅ Implement health checks
✅ Add resource limits
✅ Use hooks for migrations
✅ Test charts before deployment
✅ Version charts semantically
✅ Use .helmignore
✅ Add NOTES.txt for post-install guidance
Troubleshooting
# Debug template rendering
helm template myapp ./myapp
# Debug with values
helm template myapp ./myapp -f values-production.yaml
# Show computed values
helm get values myapp
# Show all values (including defaults)
helm get values myapp --all
# View manifest
helm get manifest myapp
# View history
helm history myapp
# Detailed status
helm status myapp
# Lint chart
helm lint ./myapp
# Validate against cluster
helm lint ./myapp --strict
Conclusion
Helm streamlines Kubernetes application management through templating, versioning, and dependency management. By following these best practices and patterns, you’ll create maintainable, reusable charts that simplify complex deployments across multiple environments.
Resources
How do you use Helm in your workflows? Share your tips in the comments!