Helm Chart Best Practices
This reference provides comprehensive best practices for creating, maintaining, and validating Helm charts.
Overview
Helm Chart Best Practices
This reference provides comprehensive best practices for creating, maintaining, and validating Helm charts.
Chart Structure
Required Files
Every Helm chart must have:
mychart/
Chart.yaml # Chart metadata
values.yaml # Default configuration values
templates/ # Template directory
Chart.yaml Structure
Use apiVersion v2 for Helm 3+ charts:
apiVersion: v2
name: mychart
description: A Helm chart for Kubernetes
type: application # or 'library' for helper charts
version: 0.1.0 # Chart version (SemVer)
appVersion: "1.16.0" # Version of the app
# Optional but recommended
keywords:
- web
- application
home: https://github.com/example/mychart
sources:
- https://github.com/example/mychart
maintainers:
- name: Your Name
email: your.email@example.com
# Dependencies
dependencies:
- name: postgresql
version: "~11.6.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
# Kubernetes version constraint
kubeVersion: ">=1.21.0-0"
Template Best Practices
1. Use Named Templates (Helpers)
Define reusable templates in templates/_helpers.tpl:
Good:
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
Usage:
metadata:
name: {{ include "mychart.fullname" . }}
2. Use include Instead of template
Good:
metadata:
labels:
{{- include "mychart.labels" . | nindent 4 }}
Bad:
metadata:
labels:
{{- template "mychart.labels" . }}
Why: include allows piping the output to other functions like nindent, indent, quote, etc.
3. Always Quote String Values
Good:
env:
- name: DATABASE_HOST
value: {{ .Values.database.host | quote }}
Bad:
env:
- name: DATABASE_HOST
value: {{ .Values.database.host }}
Why: Prevents YAML parsing issues with special characters and ensures strings are treated as strings.
4. Use required for Critical Values
Good:
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
data:
password: {{ required "A valid .Values.password is required!" .Values.password | b64enc }}
Why: Fails early with a helpful error message if required values are missing.
5. Provide Defaults with default Function
Good:
replicas: {{ .Values.replicaCount | default 1 }}
Why: Makes charts more resilient and easier to use with minimal configuration.
6. Use nindent for Proper Indentation
Good:
spec:
template:
metadata:
labels:
{{- include "mychart.labels" . | nindent 8 }}
Bad:
spec:
template:
metadata:
labels:
{{ include "mychart.labels" . | indent 8 }}
Why: nindent adds a newline before indenting, which is usually what you want in YAML.
7. Use toYaml for Complex Structures
Good:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
Why: Allows users to specify complex YAML structures in values without template complexity.
8. Conditional Resource Creation
Good:
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
# ... ingress definition
{{- end }}
Why: Allows users to optionally enable/disable resources.
9. Use with for Scoping
Good:
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 2 }}
{{- end }}
Why: Changes the scope of . to the specified value, making templates cleaner.
10. Template Comments
Good:
{{- /*
This helper creates the fullname for resources.
It supports nameOverride and fullnameOverride values.
*/ -}}
{{- define "mychart.fullname" -}}
{{- end }}
Bad:
# This creates the fullname
{{- define "mychart.fullname" -}}
{{- end }}
Why: Template comments ({{- /* */ -}}) are removed during rendering, while YAML comments (#) remain in output.
Values File Best Practices
1. Use Flat Structures When Possible
Good (Simple):
replicaCount: 1
imagePullPolicy: IfNotPresent
Good (Related Settings):
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.21.0"
Bad (Overly Nested):
app:
deployment:
pod:
container:
image:
repository: nginx
2. Document All Values
Good:
# replicaCount is the number of pod replicas for the deployment
replicaCount: 1
# image configures the container image
image:
# image.repository is the container image registry and name
repository: nginx
# image.pullPolicy is the image pull policy
pullPolicy: IfNotPresent
# image.tag overrides the image tag (default is chart appVersion)
tag: "1.21.0"
Why: Makes charts self-documenting and easier to use.
3. Provide Sensible Defaults
Good:
replicaCount: 1
service:
type: ClusterIP
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
Why: Charts should work out of the box with minimal configuration.
4. Use Boolean Flags for Feature Toggles
Good:
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
Why: Makes it clear when features are optional and how to enable them.
5. Group Related Configuration
Good:
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
6. Provide Empty Structures for Optional Config
Good:
nodeSelector: {}
tolerations: []
affinity: {}
Why: Shows users what optional configurations are available.
Kubernetes Resource Best Practices
1. Always Set Resource Limits and Requests
Good:
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
Why: Ensures proper scheduling and prevents resource exhaustion.
2. Use Proper Label Conventions
Good:
metadata:
labels:
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "mychart.chart" . }}
Why: Follows Kubernetes recommended labels for better tooling integration.
3. Use SecurityContext
Good:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
Why: Improves security posture.
4. Define Probes
Good:
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
Why: Ensures Kubernetes can properly manage application health.
5. Use ConfigMaps and Secrets Appropriately
Good:
# ConfigMap for non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}
data:
app.conf: |
{{- .Values.config | nindent 4 }}
# Secret for sensitive data
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mychart.fullname" . }}
type: Opaque
data:
password: {{ .Values.password | b64enc }}
Template Function Reference
String Functions
quote- Quote a stringsquote- Single quote a stringtrim- Remove whitespacetrimSuffix- Remove suffixtrimPrefix- Remove prefixupper- Convert to uppercaselower- Convert to lowercasetitle- Title casetrunc- Truncate stringrepeat- Repeat stringsubstr- Substringnospace- Remove all whitespace
Type Conversion
toYaml- Convert to YAMLfromYaml- Parse YAMLtoJson- Convert to JSONfromJson- Parse JSONtoString- Convert to stringtoStrings- Convert list to strings
Flow Control
default- Provide default valuerequired- Require a valuefail- Fail with error messagecoalesce- Return first non-empty value
Collections
list- Create a listdict- Create a dictionarymerge- Merge dictionariespick- Pick keys from dictionaryomit- Omit keys from dictionarykeys- Get dictionary keysvalues- Get dictionary values
Encoding
b64enc- Base64 encodeb64dec- Base64 decodesha256sum- SHA256 hash
Testing Best Practices
1. Create Test Resources
Good:
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "mychart.fullname" . }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
Run tests:
helm test <release-name>
2. Test with Multiple Values
# Test with production values
helm template my-release ./mychart -f values-prod.yaml
# Test with development values
helm template my-release ./mychart -f values-dev.yaml
# Test with overrides
helm template my-release ./mychart --set replicaCount=3
3. Validate Before Installing
# Lint the chart
helm lint ./mychart
# Dry-run install
helm install my-release ./mychart --dry-run --debug
# Validate against cluster
helm install my-release ./mychart --dry-run
Security Best Practices
1. Don't Hardcode Secrets
Bad:
data:
password: cGFzc3dvcmQ= # Don't do this!
Good:
data:
password: {{ .Values.password | b64enc }}
2. Use RBAC
Good:
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "mychart.serviceAccountName" . }}
{{- end }}
3. Run as Non-Root
Good:
securityContext:
runAsNonRoot: true
runAsUser: 1000
4. Drop Capabilities
Good:
securityContext:
capabilities:
drop:
- ALL
Performance Best Practices
1. Use .helmignore
Exclude unnecessary files from the chart package:
# .helmignore
.git/
.gitignore
*.md
.DS_Store
*.swp
test/
2. Minimize Template Complexity
Good:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
Bad:
resources:
{{- if .Values.resources.limits }}
limits:
{{- if .Values.resources.limits.cpu }}
cpu: {{ .Values.resources.limits.cpu }}
{{- end }}
{{- if .Values.resources.limits.memory }}
memory: {{ .Values.resources.limits.memory }}
{{- end }}
{{- end }}
# ... more nested conditionals
3. Use Helpers for Repeated Logic
Don't repeat the same template logic in multiple places - extract it to a helper.
Version Control Best Practices
1. Use SemVer for Chart Versions
MAJOR.MINOR.PATCH- Increment MAJOR for breaking changes
- Increment MINOR for new features
- Increment PATCH for bug fixes
2. Maintain a CHANGELOG
Document changes between versions.
3. Tag Releases
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
Common Pitfalls to Avoid
1. Not Using - for Whitespace Control
Bad:
{{ if .Values.enabled }}
key: value
{{ end }}
Good:
{{- if .Values.enabled }}
key: value
{{- end }}
2. Not Truncating Resource Names
Kubernetes resource names must be <= 63 characters:
Bad:
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
Good:
name: {{ include "mychart.fullname" . | trunc 63 | trimSuffix "-" }}
3. Using template Instead of include
Use include when you need to pipe the output to other functions.
4. Not Validating User Input
Bad:
replicas: {{ .Values.replicaCount }}
Good:
replicas: {{ required "replicaCount is required" .Values.replicaCount }}
5. Hardcoding Values in Templates
Bad:
image: nginx:1.21.0
Good:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
Upgrade and Migration Best Practices
1. Use Helm Hooks
metadata:
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": hook-succeeded
Available hooks:
pre-installpost-installpre-upgradepost-upgradepre-deletepost-deletepre-rollbackpost-rollbacktest
2. Test Upgrades
# Show what would change
helm diff upgrade my-release ./mychart
# Dry-run upgrade
helm upgrade my-release ./mychart --dry-run --debug
3. Support Rolling Back
Ensure your charts support rollback by not using hooks that delete critical resources.
Documentation Best Practices
1. Create a Comprehensive README
Include:
- Chart description
- Prerequisites
- Installation instructions
- Configuration options (values)
- Examples
- Upgrade notes
2. Document Template Functions
Add comments to your _helpers.tpl:
{{- /*
mychart.fullname generates a fully qualified application name.
It supports fullnameOverride and nameOverride values.
Maximum length is 63 characters per DNS naming spec.
Usage: {{ include "mychart.fullname" . }}
*/ -}}
{{- define "mychart.fullname" -}}
{{- end }}
3. Provide NOTES.txt
Create templates/NOTES.txt with post-installation instructions:
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get all {{ .Release.Name }}
{{- if .Values.ingress.enabled }}
Application URL:
{{- range .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
{{- end }}
{{- end }}
Packaging and Distribution
1. Package the Chart
helm package ./mychart
2. Create Chart Repository
# Create index
helm repo index .
# Serve repository
helm serve
3. Publish to Artifact Hub
Create artifacthub-repo.yml in your repository:
repositoryID: <uuid>
owners:
- name: Your Name
email: your.email@example.com
Summary Checklist
Before releasing a chart, verify:
- Chart.yaml has all required fields
- values.yaml has sensible defaults
- All values are documented
- Te