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 installed
  • post-install: After all resources are installed
  • pre-delete: Before any resources are deleted
  • post-delete: After all resources are deleted
  • pre-upgrade: Before resources are upgraded
  • post-upgrade: After all resources are upgraded
  • pre-rollback: Before resources are rolled back
  • post-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!

Continue Reading