Skip to content
Vladimir Chavkov
Go back

Helm: Complete Kubernetes Package Manager and Chart Development Guide

Edit page

Helm: Complete Kubernetes Package Manager and Chart Development Guide

Helm is the package manager for Kubernetes that simplifies deploying and managing applications. Often called “the apt/yum for Kubernetes,” Helm uses packages called Charts to define, install, and upgrade even the most complex Kubernetes applications. This comprehensive guide covers everything from basic usage to advanced chart development.

What is Helm?

Helm provides:

Key Features

  1. Package Management: Install applications with a single command
  2. Templating: Dynamic Kubernetes manifests with Go templates
  3. Version Control: Track and manage releases
  4. Rollback: Easily revert to previous versions
  5. Repository Management: Share and discover charts
  6. Dependency Management: Handle complex application dependencies
  7. Hooks: Execute operations at specific points in release lifecycle
  8. Values: Customize deployments without modifying charts

Helm Architecture

┌──────────────────────────────────────────────────────────┐
│ Helm CLI │
│ │
│ • helm install │
│ • helm upgrade │
│ • helm rollback │
│ • helm uninstall │
└────────────────────┬─────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Kubernetes API Server │
└────────────────────┬─────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Helm Release Storage │
│ (Secrets/ConfigMaps in Kubernetes) │
│ │
│ • Release metadata │
│ • Deployed manifests │
│ • Release history │
└──────────────────────────────────────────────────────────┘

Installation

macOS

Terminal window
# Using Homebrew
brew install helm
# Verify installation
helm version

Linux

Terminal window
# Using script
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Using snap
sudo snap install helm --classic
# From binary
wget https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz
tar -zxvf helm-v3.14.0-linux-amd64.tar.gz
sudo mv linux-amd64/helm /usr/local/bin/helm
# Verify
helm version

Windows

Terminal window
# Using Chocolatey
choco install kubernetes-helm
# Using Scoop
scoop install helm
# Verify
helm version

Getting Started

Repository Management

Terminal window
# Add official Helm stable repository
helm repo add stable https://charts.helm.sh/stable
# Add popular repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add jetstack https://charts.jetstack.io
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
# List repositories
helm repo list
# Update repositories
helm repo update
# Search for charts
helm search repo nginx
helm search repo --versions nginx # Show all versions
# Search Artifact Hub
helm search hub prometheus
# Remove repository
helm repo remove stable

Installing Charts

Terminal window
# Install a chart
helm install my-release bitnami/nginx
# Install with custom values
helm install my-release bitnami/nginx \
--set service.type=LoadBalancer \
--set replicaCount=3
# Install with values file
helm install my-release bitnami/nginx -f values.yaml
# Install in specific namespace
helm install my-release bitnami/nginx -n production --create-namespace
# Install with custom release name
helm install web-server bitnami/nginx
# Dry run (see what would be installed)
helm install my-release bitnami/nginx --dry-run --debug
# Generate manifests without installing
helm template my-release bitnami/nginx > manifests.yaml

Managing Releases

Terminal window
# List releases
helm list
helm list -n production
helm list --all-namespaces
helm list -a # Include uninstalled
# Get release status
helm status my-release
# Get release values
helm get values my-release
helm get values my-release --revision 2
# Get release manifest
helm get manifest my-release
# Get release notes
helm get notes my-release
# Get all release information
helm get all my-release
# Upgrade release
helm upgrade my-release bitnami/nginx \
--set replicaCount=5
# Upgrade with new values file
helm upgrade my-release bitnami/nginx -f new-values.yaml
# Upgrade or install (atomic operation)
helm upgrade --install my-release bitnami/nginx
# Rollback to previous version
helm rollback my-release
# Rollback to specific revision
helm rollback my-release 2
# Uninstall release
helm uninstall my-release
# Uninstall but keep history
helm uninstall my-release --keep-history
# View release history
helm history my-release

Creating Charts

Chart Structure

Terminal window
# Create new chart
helm create my-app
# Chart structure
my-app/
├── Chart.yaml # Chart metadata
├── values.yaml # Default configuration values
├── charts/ # Chart dependencies
├── templates/ # Kubernetes manifest templates
├── NOTES.txt # Post-installation notes
├── _helpers.tpl # Template helpers
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── hpa.yaml
└── serviceaccount.yaml
└── .helmignore # Files to ignore

Chart.yaml

Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.0.0 # Chart version
appVersion: "2.0.0" # Application version
# Optional fields
home: https://example.com
sources:
- https://github.com/example/my-app
keywords:
- web
- application
maintainers:
- name: John Doe
email: john@example.com
icon: https://example.com/icon.png
# Dependencies
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

values.yaml

# values.yaml - Default values
replicaCount: 3
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.21.6"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
service:
type: ClusterIP
port: 80
annotations: {}
ingress:
enabled: false
className: "nginx"
annotations: {}
# kubernetes.io/ingress.class: nginx
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Database configuration
postgresql:
enabled: true
auth:
database: myapp
username: myapp
primary:
persistence:
enabled: true
size: 10Gi
redis:
enabled: true
architecture: standalone
auth:
enabled: false

Template Examples

Deployment Template

templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "my-app.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: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
- name: DATABASE_HOST
value: {{ include "my-app.fullname" . }}-postgresql
- name: DATABASE_NAME
value: {{ .Values.postgresql.auth.database }}
- name: DATABASE_USER
value: {{ .Values.postgresql.auth.username }}
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "my-app.fullname" . }}-postgresql
key: password
{{- if .Values.redis.enabled }}
- name: REDIS_HOST
value: {{ include "my-app.fullname" . }}-redis-master
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

Service Template

templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "my-app.selectorLabels" . | nindent 4 }}

Helpers Template

templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "my-app.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 "my-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
{{ include "my-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "my-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "my-app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Advanced Features

Hooks

templates/hooks/pre-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-pre-install
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: pre-install
image: busybox
command: ['sh', '-c', 'echo Pre-install hook']
---
# Post-install hook
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-post-install
annotations:
"helm.sh/hook": post-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
restartPolicy: Never
containers:
- name: post-install
image: curlimages/curl
command:
- sh
- -c
- |
echo "Waiting for service to be ready..."
until curl -f http://{{ include "my-app.fullname" . }}:{{ .Values.service.port }}/health; do
sleep 5
done

Available hooks:

Conditionals and Loops

# Conditional rendering
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
# ...
{{- end }}
# With/without blocks
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
# Range/loops
{{- range .Values.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
{{- end }}
{{- end }}
# Complex conditionals
{{- if and .Values.ingress.enabled .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}

Functions

# String functions
{{ .Values.name | upper }}
{{ .Values.name | lower }}
{{ .Values.name | title }}
{{ .Values.name | trim }}
{{ .Values.name | trimSuffix "-" }}
{{ .Values.name | trunc 63 }}
{{ .Values.name | quote }}
{{ .Values.name | squote }}
# Default values
{{ .Values.name | default "my-app" }}
{{ .Values.name | required "Name is required!" }}
# Type conversion
{{ .Values.port | int }}
{{ .Values.enabled | toString }}
# Lists
{{ list "a" "b" "c" }}
{{ .Values.items | first }}
{{ .Values.items | last }}
{{ .Values.items | uniq }}
{{ .Values.items | sortAlpha }}
# Dictionaries
{{ dict "key1" "value1" "key2" "value2" }}
{{ .Values.config | keys }}
{{ .Values.config | values }}
# File functions
{{ .Files.Get "config/app.conf" }}
{{ .Files.Glob "config/*.conf" }}
{{ (.Files.Glob "config/*.conf").AsConfig }}
{{ (.Files.Glob "config/*.conf").AsSecrets }}
# Encoding
{{ .Values.data | b64enc }}
{{ .Values.encoded | b64dec }}
{{ .Values.data | sha256sum }}
# Date
{{ now | date "2006-01-02" }}
{{ now | unixEpoch }}

Testing Charts

Lint Chart

Terminal window
# Lint chart
helm lint my-app
# Lint with values
helm lint my-app -f values-prod.yaml
# Lint with strict mode
helm lint my-app --strict

Test Templates

Terminal window
# Generate templates
helm template my-app ./my-app
# Generate with values
helm template my-app ./my-app -f values-prod.yaml
# Generate specific template
helm template my-app ./my-app -s templates/deployment.yaml
# Debug template rendering
helm template my-app ./my-app --debug

Helm Test

templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "my-app.fullname" . }}-test-connection"
labels:
{{- include "my-app.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "my-app.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
Terminal window
# Run tests
helm test my-release
# Run tests with logs
helm test my-release --logs

Chart Repositories

Create Chart Repository

Terminal window
# Package chart
helm package my-app
# Create repository index
helm repo index . --url https://charts.example.com
# The index.yaml file is created
# Upload both the .tgz file and index.yaml to web server

Using Chart Museum

Terminal window
# Install ChartMuseum
helm repo add chartmuseum https://chartmuseum.github.io/charts
helm install chartmuseum chartmuseum/chartmuseum
# Upload chart
curl --data-binary "@my-app-1.0.0.tgz" http://chartmuseum:8080/api/charts
# Add repo
helm repo add my-charts http://chartmuseum:8080
helm repo update
# Install from repo
helm install my-release my-charts/my-app

Harbor as Helm Registry

Terminal window
# Login to Harbor
helm registry login harbor.example.com
# Package and push
helm package my-app
helm push my-app-1.0.0.tgz oci://harbor.example.com/library
# Install from OCI registry
helm install my-release oci://harbor.example.com/library/my-app --version 1.0.0

Production Best Practices

Values Organization

Terminal window
# Separate values per environment
values/
├── values.yaml # Base values
├── values-dev.yaml # Development overrides
├── values-staging.yaml # Staging overrides
└── values-prod.yaml # Production overrides
# Install with environment-specific values
helm install my-release ./my-app \
-f values/values.yaml \
-f values/values-prod.yaml

Secret Management

templates/external-secret.yaml
# Use external secret management
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "my-app.fullname" . }}
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: {{ include "my-app.fullname" . }}-secret
data:
- secretKey: database-password
remoteRef:
key: /secret/data/database
property: password

Version Management

Chart.yaml
# Use semantic versioning
version: 1.2.3 # MAJOR.MINOR.PATCH
# Document changes
# CHANGELOG.md
## [1.2.3] - 2026-02-11
### Added
- New ingress configuration options
### Changed
- Updated default resource limits
### Fixed
- Fixed deployment selector labels

CI/CD Integration

.github/workflows/helm.yml
name: Helm Chart CI
on:
push:
paths:
- 'charts/**'
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: v3.14.0
- name: Lint chart
run: helm lint charts/my-app
- name: Template chart
run: helm template test charts/my-app
- name: Package chart
run: helm package charts/my-app
- name: Upload to ChartMuseum
if: github.ref == 'refs/heads/main'
run: |
curl --data-binary "@my-app-*.tgz" \
-u "${{ secrets.CHARTMUSEUM_USER }}:${{ secrets.CHARTMUSEUM_PASSWORD }}" \
https://charts.example.com/api/charts

Troubleshooting

Common Issues

Terminal window
# Debug installation
helm install my-release ./my-app --dry-run --debug
# Check release status
helm status my-release
helm get manifest my-release
# View release history
helm history my-release
# Check values
helm get values my-release
# Validate rendered templates
helm template my-release ./my-app | kubectl apply --dry-run=client -f -
# Force uninstall stuck release
kubectl delete secret -l owner=helm,status=deployed
# Reset Helm storage
kubectl delete secret -l owner=helm -n <namespace>

Conclusion

Helm simplifies Kubernetes application management with powerful templating, version control, and package management capabilities. By mastering Helm chart development and following best practices, teams can efficiently deploy and manage complex applications across multiple environments.


Master Kubernetes tools including Helm with our comprehensive Kubernetes training programs. Contact us for customized training designed for your team’s needs.


Edit page
Share this post on:

Previous Post
ArgoCD: Complete GitOps Continuous Delivery Guide for Kubernetes
Next Post
K9s: The Ultimate Kubernetes Terminal UI for Cluster Management