Helm Charts: Templating, Values, and Package Management
Learn Helm Charts from basics to advanced patterns. Master Helm templates, values management, chart repositories, and production deployment workflows.
Helm Charts: Templating, Values, and Kubernetes Package Management
Helm is the package manager for Kubernetes. If you have spent time deploying applications to a Kubernetes cluster manually, you know how tedious it becomes to manage multiple YAML files across different environments. Helm solves this by letting you package everything into a chart that can be versioned, shared, and deployed with a single command.
This guide covers everything from creating basic charts to advanced templating patterns. If you are new to Kubernetes containers, start with our Docker Fundamentals and Advanced Kubernetes guides first.
Helm Architecture
Helm has a client-server architecture. The Helm client interacts with the server-side component called Tiller in Helm 2, or uses the cluster’s service account in Helm 3.
graph LR
A[Helm Client] -->|Chart| B[Chart Repository]
A -->|Kubeconfig| C[Kubernetes Cluster]
C -->|Release| D[Release History]
C -->|Resources| E[Deployed Resources]
Helm 3 removed Tiller and introduced improved security with cluster-scoped service accounts and release-level scope. This made Helm significantly easier to operate in production.
Chart Structure
A chart is a collection of files organized in a specific directory structure:
my-chart/
Chart.yaml # Chart metadata
values.yaml # Default configuration values
values.schema.json # Optional JSON schema validation
charts/ # Dependency charts (Helm 3 style)
templates/ # Kubernetes manifest templates
templates/NOTES.txt # Post-install notes
.helmignore # Files to ignore during packaging
Chart.yaml
The Chart.yaml contains the chart’s metadata:
apiVersion: v2
name: my-application
description: A Helm chart for my production application
type: application
version: 1.2.3
appVersion: "2.0.0"
kubeVersion: ">=1.24.0"
keywords:
- web application
- api
home: https://github.com/example/my-app
sources:
- https://github.com/example/my-app
maintainers:
- name: DevOps Team
email: devops@example.com
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com"
condition: postgresql.enabled
- name: redis
version: "17.x.x"
repository: "https://charts.bitnami.com"
condition: redis.enabled
The apiVersion: v2 format is for Helm 3. Helm 2 used apiVersion: v1.
Values Files
Values files provide configuration that gets merged into templates. Helm uses a cascading values system: default values in values.yaml, overridden by environment-specific files, overridden by command-line flags.
values.yaml
# Default configuration
replicaCount: 3
repository: myregistry/myapp
pullPolicy: IfNotPresent
tag: "1.0.0"
service:
type: ClusterIP
port: 8080
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- api.example.com
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 100m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
postgresql:
enabled: true
auth:
database: myapp
username: myapp
primary:
persistence:
size: 10Gi
redis:
enabled: true
auth:
password: ""
Environment-Specific Values
Create environment-specific value files:
# values-staging.yaml
replicaCount: 2
tag: "1.0.0-staging"
resources:
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
ingress:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
hosts:
- host: staging-api.example.com
# values-prod.yaml
replicaCount: 5
tag: "1.0.0-production"
resources:
limits:
cpu: 2000m
memory: 2Gi
autoscaling:
minReplicas: 5
maxReplicas: 20
Deploy with specific values:
helm upgrade --install myapp ./charts/myapp \
--values values-prod.yaml \
--namespace production \
--create-namespace
Template Functions and Pipelines
Helm templates use Go template syntax with Sprig functions for manipulation.
Common Functions
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /ready
port: http
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
String Functions
# Uppercase
appName: { { .Values.name | upper } }
# Lowercase
envVar: { { .Values.env | lower | squote } }
# Truncate
shortName: { { .Values.name | trunc 63 } }
# Replace
fixedName: { { .Values.name | replace "_" "-" } }
# Quote
quoted: { { .Values.name | quote } }
# Default value
tag: { { .Values.image.tag | default .Chart.AppVersion } }
Conditional Logic
# Ternary operator
replicas: {{ .Values.replicaCount | default 1 }}
# If-else blocks
{{- if .Values.ingress.enabled }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
spec:
{{- with .Values.ingress }}
ingressClassName: {{ .className }}
{{- end }}
{{- end }}
Range Loops
# Loop over list
env:
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
# Loop over key-value map
labels:
{{- range $key, $value := .Values.labels }}
{{ $key }}: {{ $value | quote }}
{{- end }}
# Loop with index
{{- range $i, $v := .Values.replicas }}
- name: replica-{{ $i }}
{{- end }}
Named Templates
Named templates (partials) live in templates/_helpers.tpl and can be included throughout your chart.
Defining Helpers
# templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Full name including release and chart.
*/}}
{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels.
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{ include "myapp.name" . }}: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ include "myapp.name" . }}
{{- end }}
{{/*
Selector labels.
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Using Helpers in Templates
apiVersion: apps/v1
kind: Deployment
metadata:
name: { { include "myapp.fullname" . } }
labels: { { - include "myapp.labels" . | nindent 4 } }
spec:
replicas: { { .Values.replicaCount } }
selector:
matchLabels: { { - include "myapp.selectorLabels" . | nindent 6 } }
Chart Dependencies
Charts can depend on other charts. In Helm 3, dependencies are defined in Chart.yaml and downloaded to the charts/ directory.
Dependency Management
# Chart.yaml
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com"
condition: postgresql.enabled
- name: redis
version: "17.x.x"
repository: "https://charts.bitnami.com"
condition: redis.enabled
Update dependencies:
helm dependency update ./charts/myapp
This creates a Chart.lock file that locks dependency versions.
Sub-chart Values
Parent charts can override sub-chart values:
# values.yaml
postgresql:
primary:
persistence:
size: 50Gi
auth:
database: myapp_prod
username: myapp
redis:
auth:
password: secretpassword
master:
persistence:
enabled: false
Hooks
Hooks run at specific points in the release lifecycle. Use them for database migrations, backup jobs, or waiting for dependencies.
Common Hook Annotations
# hooks/post-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-migrations
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrations
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command:
- /app/migrate.sh
Hook Weights
Hooks execute in order of weight. Negative weights run first. Use this to ensure database migrations run before your application starts.
Testing Charts
Helm includes a test framework for validating chart installations.
Test Pod
# templates/tests/pod-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: {{ include "myapp.fullname" . }}-test-connection
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": test-success
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox:1.36
command:
- wget
args:
- '-O-'
- 'http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health'
Run tests:
helm test myapp --namespace production
Chart Repositories
Chart repositories serve packaged charts via an HTTP server. You can create your own repository for internal charts.
Creating a Repository
# Package your chart
helm package ./charts/myapp
# Create index.yaml
helm repo index ./charts --url https://charts.example.com/
# Serve repository via HTTP
python3 -m http.server 8080 --directory ./charts
Adding and Using Repositories
# Add repository
helm repo add bitnami https://charts.bitnami.com
# Update repositories
helm repo update
# Search for charts
helm search repo bitnami/postgresql
# Install from repository
helm install mydb bitnami/postgresql --values values.yaml
GitHub Pages Repository
Host your chart repository on GitHub Pages:
# In your charts repository
git checkout gh-pages || git checkout -b gh-pages
# Copy your chart package
cp ../my-chart-1.2.3.tgz ./
# Update index
helm repo index . --url https://example.github.io/charts
git add .
git commit -m "Add my-chart v1.2.3"
git push origin gh-pages
Library Charts
Library charts provide reusable templates that other charts can include. They are useful for standardizing common patterns across your organization.
Library Chart Structure
library-chart/
Chart.yaml
templates/
_deployment.yaml
_service.yaml
_configmap.yaml
# Chart.yaml
apiVersion: v2
name: myapp-library
type: library
version: 1.0.0
Using Library Chart
# In your application chart
dependencies:
- name: myapp-library
version: "1.x.x"
repository: "https://charts.example.com"
import-values:
- data
JSON Schema Validation
Values schema files validate values provided to your chart:
// values.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"maximum": 10
},
"image": {
"type": "object",
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" },
"pullPolicy": {
"type": "string",
"enum": ["IfNotPresent", "Always", "Never"]
}
},
"required": ["repository"]
},
"service": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ClusterIP", "LoadBalancer", "NodePort"]
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "port"]
}
},
"required": ["image", "service"]
}
If a user provides invalid configuration, Helm rejects it during template rendering:
helm template myapp ./charts/myapp --values invalid-values.yaml
# Error: values don't meet the schema
Best Practices
Production Deployment Checklist
Pin exact versions in dependencies. Use values.schema.json for validation. Keep values.yaml well-documented with comments. Use hooks for stateful operations like migrations. Test charts with helm template and helm test. Store secrets outside the chart and use external secrets tools.
Security Considerations
Do not embed secrets in values files. Use external secrets solutions like External Secrets Operator or HashiCorp Vault. Run containers as non-root. Scan charts for vulnerabilities with tools like Trivy or Snyk.
Release Management
Use CI/CD pipelines for chart releases. Maintain CHANGELOG.md for version history. Tag releases with Git tags matching chart versions. Sign charts with GPG for verification.
When to Use / When Not to Use
Helm is powerful but not always the right choice. Here is when to use it and when to consider alternatives.
When to Use Helm
Use Helm when:
- You deploy applications to Kubernetes across multiple environments (dev, staging, production)
- You need to manage complex applications with many Kubernetes resources
- You want to version and roll back application configurations
- You are sharing infrastructure or platform components across teams
- You need to package applications for distribution via chart repositories
- Your application has environment-specific configuration that changes between deployments
Use Helm for stateful applications when:
- The application has clear upgrade paths and rollback procedures
- You have tested hooks for database migrations or similar operations
- Storage and secrets are properly externalized from the chart
When Not to Use Helm
Consider alternatives when:
- Simple one-off deployments:
kubectl apply -fmay suffice - GitOps workflows: Tools like ArgoCD or Flux have built-in templating and might integrate better
- Extremely dynamic workloads: Helm’s release model assumes relatively stable configurations
- Strict immutability requirements: Helm’s upgrade pattern modifies releases in place
- Security-restricted environments: Tiller in Helm 2 required elevated permissions (Helm 3 addressed most concerns)
Helm vs Alternatives
| Tool | Best For | Limitations |
|---|---|---|
| Helm | Application packaging, multi-environment deployments | Template complexity for very dynamic workloads |
| Kustomize | Overlay-based configuration, Git-native workflows | Less structured than Helm charts |
| ArgoCD | GitOps, declarative continuous delivery | Requires additional setup for templating |
| raw kubectl | One-off deployments, simple resources | No parameterization, no rollback support |
Production Failure Scenarios
Helm failures in production can block deployments or cause unexpected behavior.
| Failure | Impact | Mitigation |
|---|---|---|
| Hook failure | Release marked as failed, subsequent hooks do not run | Design hooks to be idempotent, use hook-delete-policy |
| Template rendering error | Deployment fails silently with wrong configuration | Use --dry-run in CI, validate with JSON schema |
| Dependency unavailable | Chart fails to install or upgrade | Pin exact versions in Chart.lock, maintain mirror |
| Release name collision | Cannot install or upgrade without force | Use namespaces for isolation, unique naming conventions |
| Stuck release (pending-upgrade) | Cannot upgrade or rollback | Use helm rollback or manually remove finalizers |
| Upgrade causes data loss | Stateful workloads may lose data | Always test with dry-run first, backup before upgrade |
| Image pull failures | Pods hang in Pending state | Use private registries with image pull secrets |
| Insufficient cluster resources | Pods cannot be scheduled | Set appropriate resource requests and limits |
Stuck Release Recovery
# Identify stuck releases
helm list --all --failed
# Get release history
helm history myapp --namespace production
# Rollback to last working revision
helm rollback myapp --namespace production
# If rollback fails, manually remove the release
kubectl delete secret -n production -l "owner=helm,name=myapp"
Observability Checklist
Helm charts need observability at both the release and application level.
Release Monitoring
graph LR
A[Helm Release] --> B[Release Metadata]
A --> C[Revision History]
A --> D[Resource Status]
B --> E[Name Namespace]
B --> F[Chart Version]
C --> G[Upgrade Rollback]
D --> H[Pod Health]
D --> I[PVC Bound]
Track these release metrics:
- Release revision count (too many revisions indicate instability)
- Time since last successful deployment
- Number of failed releases across environments
- Hook execution success rates
Application Observability
graph TD
A[Application Metrics] --> B[Pod Resource Usage]
A --> C[Service Endpoints]
A --> D[Ingress Status]
E[Kubernetes Events] --> F[Pod Scheduling]
E --> G[Volume Mounts]
E --> H[Image Pulls]
Metrics to monitor:
- Pod CPU and memory actual usage vs requested
- Service endpoint availability
- Persistent volume claim status and usage
- Ingress controller backend health
- Container image versions deployed
Alert Configuration
Critical alerts:
- Helm release failed during production deployment
- Pods in CrashLoopBackOff after Helm upgrade
- Hook jobs failing repeatedly
- Release revision rapidly increasing (indicates instability)
Warning alerts:
- PVC pending for more than 5 minutes
- Deployment replica count below desired
- Image pull backoff occurring
Security Checklist
Helm charts require security review before production deployment.
Chart Security
-
Scan for vulnerabilities: Use Trivy or Snyk to scan chart dependencies
trivy chart ./charts/myapp -
Verify chart signatures: Use GPG signing for chart verification
helm verify myapp-1.0.0.tgz -
Review values files: Ensure no hardcoded secrets or insecure defaults
-
Validate values schema: Use JSON schema to enforce secure configurations
Image Security
# values.yaml security settings
image:
repository: myregistry.myapp.com/api
pullPolicy: IfNotPresent # Not Always in production
securityContext:
runAsNonRoot: true
runAsUser: 10000
podSecurityContext:
runAsNonRoot: true
fsGroup: 10000
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
Image hardening checklist:
- Use specific image tags, never
latest - Scan images for CVEs before deployment
- Use private registry with authentication
- Set
imagePullPolicyappropriately (IfNotPresent or Always)
RBAC for Helm
Helm 3 uses the permissions of the user or service account running it.
# Create dedicated SA for Helm
kubectl create serviceaccount helm-deployer -n production
# Grant only needed permissions
kubectl create role helm-deployer --verb=get,list,watch,create,update,patch,delete \
--resource=deployments,services,configmaps,secrets,pvc -n production
kubectl create rolebinding helm-deployer-binding \
--role=helm-deployer --serviceaccount=production:helm-deployer -n production
Secret Management
Never:
- Commit secrets to values files
- Store secrets in Chart.yaml or templates
- Use ConfigMap for sensitive data
Always:
- Use External Secrets Operator or Vault
- Reference secrets from external secret stores
- Use
--set-filefor certificate contents
Common Pitfalls / Anti-Patterns
Template Pitfalls
Overusing toYaml
# Anti-pattern: Unbounded toYaml
spec:
{{- toYaml .Values.podSpec | nindent 8 }}
# Better: Explicitly define expected fields
spec:
replicas: {{ .Values.replicaCount }}
selector:
{{- include "myapp.selectorLabels" . | nindent 6 }}
Not handling nil values
# Anti-pattern: Will fail if .Values.ingress is nil
annotations:
{{- .Values.ingress.annotations | toYaml | nindent 8 }}
# Better: Use conditional checks
{{- with .Values.ingress }}
annotations:
{{- .annotations | toYaml | nindent 8 }}
{{- end }}
Forgetting the minus sign for whitespace control
# Anti-pattern: Extra blank line before content
spec:
containers:
- name: app
image: {{ .Values.image }}
# Better: Hyphen trims preceding whitespace
spec:
containers:
- name: app
image: {{ .Values.image }}
Values Structure Pitfalls
Flat values that should be nested
# Anti-pattern: Flat structure makes templating complex
replicaCount: 3
imageRepository: myapp
imageTag: "1.0.0"
servicePort: 8080
# Better: Group related values
replicaCount: 3
image:
repository: myapp
tag: "1.0.0"
service:
type: ClusterIP
port: 8080
Not using JSON schema validation Without schema validation, users can pass any values causing cryptic template errors at render time.
Hook Pitfalls
Non-idempotent hooks
# Anti-pattern: Hook creates duplicate resources each run
metadata:
annotations:
"helm.sh/hook": post-upgrade
"helm.sh/hook-weight": "1"
spec:
containers:
- name: migrate
command: ["/app/migrate.sh"]
# This runs every upgrade, creating duplicates if not cleaned up
Not handling hook failures gracefully
# Anti-pattern: Hook failure blocks entire release
annotations:
"helm.sh/hook": post-upgrade
"helm.sh/hook-failure-policy": fail
# Better: Allow failure without blocking
annotations:
"helm.sh/hook": post-upgrade
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
Dependency Pitfalls
Not pinning versions
# Anti-pattern: Version range can cause unexpected behavior
dependencies:
- name: postgresql
version: ">=12.0.0"
# Better: Pin exact version for reproducibility
dependencies:
- name: postgresql
version: "12.8.0"
Circular dependencies Avoid charts that depend on each other. This causes installation and upgrade ordering issues.
Quick Recap
Key Takeaways
- Helm charts package Kubernetes resources with templated values for environment-specific configuration
- The cascading values system (default < environment file < CLI flags) enables flexible deployments
- Named templates in
_helpers.tplpromote consistency and reduce duplication - Hooks handle stateful operations like migrations but require careful design for idempotency
- Library charts extract common patterns for reuse across multiple application charts
- JSON schema validation prevents misconfiguration before template rendering
Production Readiness Checklist
# Template validation
helm template myapp ./charts/myapp --debug
# Dry-run installation
helm upgrade --install myapp ./charts/myapp \
--dry-run --debug \
--values values-prod.yaml \
--namespace production
# Schema validation
helm lint ./charts/myapp --strict
# Test installation
helm test myapp --namespace production
# Dependency update and lock
helm dependency update ./charts/myapp
helm dependency build ./charts/myapp
# Verify rendered templates
helm get manifest myapp --namespace production
# Check release status
helm status myapp --namespace production
helm history myapp --namespace production
Pre-Production Validation Commands
# Scan for vulnerabilities
trivy image $(helm get values myapp -n production -o jsonpath='{.data.image}' | base64 -d)
# Verify RBAC permissions
kubectl auth can-i get pods --as=system:serviceaccount:production:helm-deployer -n production
# Check for deprecated APIs
helm template myapp ./charts/myapp | kubeval --strict
# Review rendered YAML differences
helm diff upgrade myapp ./charts/myapp --values values-prod.yaml -n production
Trade-off Summary
| Aspect | Helm | Kustomize | Carvel (kapp-controller) |
|---|---|---|---|
| Model | Template + values | Overlay + patches | Package + config |
| Learning curve | Moderate (Go templates) | Low (YAML only) | Moderate |
| Reusable packages | Yes (chart repos) | Limited (git-based) | Yes (imgpkg bundles) |
| Debugging | Render and inspect | Build and diff | Build and inspect |
| Ecosystem | Massive (Bitnami) | Growing | Smaller but maturing |
| GitOps friendly | Yes (with Flux/ArgoCD) | Native | Native |
| Secret management | Values encryption | Sealed secrets | Bank-Vaults |
Conclusion
Helm simplifies Kubernetes deployments through templating and package management. The chart structure, template functions, and dependency system provide flexibility while maintaining consistency across environments.
Start by converting your existing Kubernetes manifests to a chart. Add templating for environment-specific values. Then extract reusable components into library charts as patterns emerge.
For continued learning, explore the Advanced Kubernetes guide for operators and controllers that work alongside Helm. The Prometheus & Grafana guides cover observability for your Helm-deployed applications.
Category
Related Posts
Developing Helm Charts: Templates, Values, and Testing
Create production-ready Helm charts with Go templates, custom value schemas, and testing using Helm unittest and ct.
Helm Versioning and Rollback: Managing Application Releases
Master Helm release management—revision history, automated rollbacks, rollback strategies, and handling failed releases gracefully.
Advanced Kubernetes: Controllers, Operators, RBAC, Production Patterns
Explore Kubernetes custom controllers, operators, RBAC, network policies, storage classes, and advanced patterns for production cluster management.