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
- Package Management: Install applications with a single command
- Templating: Dynamic Kubernetes manifests with Go templates
- Version Control: Track and manage releases
- Rollback: Easily revert to previous versions
- Repository Management: Share and discover charts
- Dependency Management: Handle complex application dependencies
- Hooks: Execute operations at specific points in release lifecycle
- 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
# Using Homebrewbrew install helm
# Verify installationhelm versionLinux
# Using scriptcurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Using snapsudo snap install helm --classic
# From binarywget https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gztar -zxvf helm-v3.14.0-linux-amd64.tar.gzsudo mv linux-amd64/helm /usr/local/bin/helm
# Verifyhelm versionWindows
# Using Chocolateychoco install kubernetes-helm
# Using Scoopscoop install helm
# Verifyhelm versionGetting Started
Repository Management
# Add official Helm stable repositoryhelm repo add stable https://charts.helm.sh/stable
# Add popular repositorieshelm repo add bitnami https://charts.bitnami.com/bitnamihelm repo add jetstack https://charts.jetstack.iohelm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo add prometheus-community https://prometheus-community.github.io/helm-chartshelm repo add grafana https://grafana.github.io/helm-charts
# List repositorieshelm repo list
# Update repositorieshelm repo update
# Search for chartshelm search repo nginxhelm search repo --versions nginx # Show all versions
# Search Artifact Hubhelm search hub prometheus
# Remove repositoryhelm repo remove stableInstalling Charts
# Install a charthelm install my-release bitnami/nginx
# Install with custom valueshelm install my-release bitnami/nginx \ --set service.type=LoadBalancer \ --set replicaCount=3
# Install with values filehelm install my-release bitnami/nginx -f values.yaml
# Install in specific namespacehelm install my-release bitnami/nginx -n production --create-namespace
# Install with custom release namehelm install web-server bitnami/nginx
# Dry run (see what would be installed)helm install my-release bitnami/nginx --dry-run --debug
# Generate manifests without installinghelm template my-release bitnami/nginx > manifests.yamlManaging Releases
# List releaseshelm listhelm list -n productionhelm list --all-namespaceshelm list -a # Include uninstalled
# Get release statushelm status my-release
# Get release valueshelm get values my-releasehelm get values my-release --revision 2
# Get release manifesthelm get manifest my-release
# Get release noteshelm get notes my-release
# Get all release informationhelm get all my-release
# Upgrade releasehelm upgrade my-release bitnami/nginx \ --set replicaCount=5
# Upgrade with new values filehelm upgrade my-release bitnami/nginx -f new-values.yaml
# Upgrade or install (atomic operation)helm upgrade --install my-release bitnami/nginx
# Rollback to previous versionhelm rollback my-release
# Rollback to specific revisionhelm rollback my-release 2
# Uninstall releasehelm uninstall my-release
# Uninstall but keep historyhelm uninstall my-release --keep-history
# View release historyhelm history my-releaseCreating Charts
Chart Structure
# Create new charthelm create my-app
# Chart structuremy-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 ignoreChart.yaml
apiVersion: v2name: my-appdescription: A Helm chart for my applicationtype: applicationversion: 1.0.0 # Chart versionappVersion: "2.0.0" # Application version
# Optional fieldshome: https://example.comsources: - https://github.com/example/my-appkeywords: - web - applicationmaintainers: - name: John Doe email: john@example.comicon: https://example.com/icon.png
# Dependenciesdependencies: - 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.enabledvalues.yaml
# values.yaml - Default valuesreplicaCount: 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 configurationpostgresql: enabled: true auth: database: myapp username: myapp primary: persistence: enabled: true size: 10Gi
redis: enabled: true architecture: standalone auth: enabled: falseTemplate Examples
Deployment Template
apiVersion: apps/v1kind: Deploymentmetadata: 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
apiVersion: v1kind: Servicemetadata: 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
{{/*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
apiVersion: batch/v1kind: Jobmetadata: name: {{ include "my-app.fullname" . }}-pre-install annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "-5" "helm.sh/hook-delete-policy": hook-succeededspec: template: spec: restartPolicy: Never containers: - name: pre-install image: busybox command: ['sh', '-c', 'echo Pre-install hook']---# Post-install hookapiVersion: batch/v1kind: Jobmetadata: 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-creationspec: 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 doneAvailable hooks:
pre-install: Before any resources are installedpost-install: After all resources are installedpre-delete: Before any resources are deletedpost-delete: After all resources are deletedpre-upgrade: Before any resources are upgradedpost-upgrade: After all resources are upgradedpre-rollback: Before any resources are rolled backpost-rollback: After all resources are rolled backtest: Whenhelm testis invoked
Conditionals and Loops
# Conditional rendering{{- if .Values.ingress.enabled }}apiVersion: networking.k8s.io/v1kind: 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
# Lint charthelm lint my-app
# Lint with valueshelm lint my-app -f values-prod.yaml
# Lint with strict modehelm lint my-app --strictTest Templates
# Generate templateshelm template my-app ./my-app
# Generate with valueshelm template my-app ./my-app -f values-prod.yaml
# Generate specific templatehelm template my-app ./my-app -s templates/deployment.yaml
# Debug template renderinghelm template my-app ./my-app --debugHelm Test
apiVersion: v1kind: Podmetadata: name: "{{ include "my-app.fullname" . }}-test-connection" labels: {{- include "my-app.labels" . | nindent 4 }} annotations: "helm.sh/hook": testspec: containers: - name: wget image: busybox command: ['wget'] args: ['{{ include "my-app.fullname" . }}:{{ .Values.service.port }}'] restartPolicy: Never# Run testshelm test my-release
# Run tests with logshelm test my-release --logsChart Repositories
Create Chart Repository
# Package charthelm package my-app
# Create repository indexhelm repo index . --url https://charts.example.com
# The index.yaml file is created# Upload both the .tgz file and index.yaml to web serverUsing Chart Museum
# Install ChartMuseumhelm repo add chartmuseum https://chartmuseum.github.io/chartshelm install chartmuseum chartmuseum/chartmuseum
# Upload chartcurl --data-binary "@my-app-1.0.0.tgz" http://chartmuseum:8080/api/charts
# Add repohelm repo add my-charts http://chartmuseum:8080helm repo update
# Install from repohelm install my-release my-charts/my-appHarbor as Helm Registry
# Login to Harborhelm registry login harbor.example.com
# Package and pushhelm package my-apphelm push my-app-1.0.0.tgz oci://harbor.example.com/library
# Install from OCI registryhelm install my-release oci://harbor.example.com/library/my-app --version 1.0.0Production Best Practices
Values Organization
# Separate values per environmentvalues/├── values.yaml # Base values├── values-dev.yaml # Development overrides├── values-staging.yaml # Staging overrides└── values-prod.yaml # Production overrides
# Install with environment-specific valueshelm install my-release ./my-app \ -f values/values.yaml \ -f values/values-prod.yamlSecret Management
# Use external secret managementapiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: 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: passwordVersion Management
# Use semantic versioningversion: 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 labelsCI/CD Integration
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/chartsTroubleshooting
Common Issues
# Debug installationhelm install my-release ./my-app --dry-run --debug
# Check release statushelm status my-releasehelm get manifest my-release
# View release historyhelm history my-release
# Check valueshelm get values my-release
# Validate rendered templateshelm template my-release ./my-app | kubectl apply --dry-run=client -f -
# Force uninstall stuck releasekubectl delete secret -l owner=helm,status=deployed
# Reset Helm storagekubectl 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.