Helm Best Practices
A comprehensive guide to Helm best practices covering chart structure, values management, versioning, security, and production guidelines for Helm charts.
Helm
Best Practices
Security
Why Helm Best Practices Matter
Following Helm best practices ensures your charts are maintainable, secure, reusable, and production-ready. Well-designed charts reduce operational overhead, prevent common pitfalls, and enable teams to collaborate effectively.
Key areas of focus include:
- Chart Structure: Consistent organization for readability
- Values Management: Clean configuration and overrides
- Versioning: Semantic versioning for releases
- Security: Secrets, RBAC, and signing
- Testing: Linting, unit tests, integration tests
- Documentation: README and comments
Key Principle: Treat Helm charts like software artifacts. Apply the same rigor to chart development as you would to application code.
Chart Structure Best Practices
Follow Standard Structure
Use the standard Helm directory structure: Chart.yaml, values.yaml, templates/, charts/, _helpers.tpl.
Name Charts Consistently
Use lowercase, hyphenated names that match the directory name. Avoid special characters.
Document in README
Include installation instructions, values documentation, and examples in README.md.
Use Helpers
Define reusable template functions in _helpers.tpl to avoid duplication.
Keep Templates Focused
One resource per template file. Use descriptive filenames (deployment.yaml, service.yaml).
Include Values Schema
Add values.schema.json for validation. Prevents misconfigurations before installation.
# Recommended chart structure
my-app/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── values.schema.json # Values validation
├── README.md # Documentation
├── .helmignore # Ignore patterns
├── charts/ # Dependencies
├── crds/ # CRDs
├── templates/
│ ├── _helpers.tpl # Template helpers
│ ├── deployment.yaml # Deployment
│ ├── service.yaml # Service
│ ├── configmap.yaml # ConfigMap
│ ├── secret.yaml # Secret
│ ├── hpa.yaml # Autoscaler
│ ├── ingress.yaml # Ingress
│ ├── NOTES.txt # Post-install notes
│ └── tests/ # Test resources
│ └── test-connection.yaml
└── ci/ # CI test values
└── test-values.yaml
# Chart.yaml - Complete metadata
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.2.3
appVersion: 2.5.0
home: https://example.com
icon: https://example.com/icon.png
maintainers:
- name: John Doe
email: john@example.com
keywords:
- web
- microservice
sources:
- https://github.com/example/my-app
dependencies:
- name: postgresql
version: 11.x.x
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
Chart Structure Best Practices:
- Use standard directory structure for consistency
- Name charts with lowercase, hyphens
- Document everything in README.md
- Use helpers to avoid duplication
- Include values.schema.json for validation
- Add .helmignore to exclude files
- Use NOTES.txt for post-install instructions
Values Management Best Practices
Organized Values
Group related values logically. Use comments to explain complex configurations.
Sensible Defaults
Provide production-ready defaults. Charts should work out of the box for common use cases.
Environment Values
Use separate values files for dev, staging, and production (values-dev.yaml, values-prod.yaml).
Never Store Secrets
Never store secrets in values.yaml. Use external secret management solutions.
Schema Validation
Use values.schema.json to validate values and provide better error messages.
Global Values
Use global values for shared configuration like registry, storage class, and image pull secrets.
# values.yaml - Well-organized with comments
# Global settings
global:
imagePullSecrets: []
storageClass: standard
registry: docker.io
# Application settings
replicaCount: 3
image:
repository: nginx
tag: latest
pullPolicy: IfNotPresent
# Service configuration
service:
type: ClusterIP
port: 80
targetPort: 8080
# Resources
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
# Autoscaling
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# Environment-specific values
# values-dev.yaml
replicaCount: 1
image:
tag: dev
resources:
requests:
memory: "32Mi"
cpu: "100m"
# values-prod.yaml
replicaCount: 5
image:
tag: v1.2.3
resources:
requests:
memory: "256Mi"
cpu: "500m"
autoscaling:
enabled: true
minReplicas: 3
# Install with environment values
helm install my-release ./my-chart -f values-prod.yaml
# values.schema.json - Validation
{
"$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"
}
},
"required": ["repository"]
}
},
"required": ["image"]
}
Values Best Practices:
- Organize values logically with clear sections
- Provide production-ready defaults
- Use environment-specific values files
- Never store secrets in values.yaml
- Use schema validation for complex charts
- Document all values in README.md
- Use global values for shared config
Versioning Best Practices
Semantic Versioning
Use SemVer (MAJOR.MINOR.PATCH) for chart versions. Increment appropriately.
Chart vs App Version
Keep chart version and appVersion separate. Chart version for chart changes, appVersion for app changes.
Maintain History
Use --history-max to control release history. Balance auditability with storage.
Tag in Git
Tag chart releases in Git for traceability. Use Git tags for version management.
Pin Dependencies
Pin dependency versions to avoid unexpected changes. Use exact versions or narrow ranges.
Changelog
Maintain a CHANGELOG.md for each chart documenting changes between versions.
# Chart.yaml versioning
apiVersion: v2
name: my-app
version: 1.2.3 # Chart version (SemVer)
appVersion: 2.5.0 # Application version
# Versioning rules:
# MAJOR: Breaking changes
# MINOR: New features (backward compatible)
# PATCH: Bug fixes (backward compatible)
# Dependency versioning
dependencies:
- name: postgresql
version: "11.0.0" # Exact version
- name: redis
version: ">=16.0.0 <17.0.0" # Range
- name: mongodb
version: "~12.0.0" # Patch updates only
# Release history management
helm upgrade my-release ./my-chart --history-max 20
# Tag releases in Git
git tag -a v1.2.3 -m "Release 1.2.3"
git push origin v1.2.3
# CHANGELOG.md
# Changelog
## [1.2.3] - 2025-01-15
### Fixed
- Fixed resource limits in deployment
- Corrected service port configuration
## [1.2.2] - 2025-01-10
### Added
- Added HPA support
- Added ingress configuration
## [1.2.1] - 2025-01-05
### Changed
- Updated default image tag to v1.2.1
Versioning Best Practices:
- Follow semantic versioning strictly
- Keep chart and app versions separate
- Pin dependency versions
- Tag releases in Git
- Maintain a changelog
- Limit release history appropriately
Security Best Practices
Secrets Management
Use Sealed Secrets, helm-secrets, or External Secrets Operator. Never store secrets in values.yaml.
RBAC
Use least privilege for Helm service accounts. Limit permissions to specific namespaces.
Chart Signing
Sign charts with PGP keys. Verify signatures before installation.
Vulnerability Scanning
Scan charts for vulnerabilities with Trivy, kube-score, or Snyk.
Security Contexts
Define security contexts in pods. Run as non-root, drop capabilities, read-only filesystem.
Policy Enforcement
Use OPA/Gatekeeper to enforce security policies on Helm deployments.
# Security context in deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
# RBAC for Helm
apiVersion: v1
kind: ServiceAccount
metadata:
name: helm-user
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: helm-user-role
namespace: default
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Chart signing
helm package ./my-chart --sign --key "Your Name"
helm verify my-chart-1.0.0.tgz
# Vulnerability scanning
trivy config ./my-chart
kube-score score ./rendered-manifests.yaml
# Using Sealed Secrets
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: my-secret
spec:
encryptedData:
password: AgBy... # Encrypted
# Using External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-secret
spec:
secretStoreRef:
name: vault-store
kind: SecretStore
target:
name: my-secret
data:
- secretKey: password
remoteRef:
key: app/db
property: password
Security Best Practices:
- Never store secrets in values.yaml
- Use external secret management
- Apply least privilege RBAC
- Sign and verify charts
- Scan for vulnerabilities
- Define security contexts
- Enforce policies with OPA
Testing Best Practices
Lint Charts
Run helm lint before every commit. Use --strict mode for comprehensive validation.
Unit Tests
Use helm-unittest to test template rendering with assertions.
Integration Tests
Define helm test hooks to verify deployments in a cluster.
Validate Manifests
Use kubeconform or kubectl apply --dry-run to validate generated manifests.
Dry Run
Always use --dry-run --debug before actual installation.
CI/CD Testing
Run linting, unit tests, and validation in CI/CD pipelines.
# Linting
helm lint ./my-chart --strict
helm lint ./my-chart -f values-prod.yaml
# Unit testing with helm-unittest
helm plugin install https://github.com/helm-unittest/helm-unittest
helm unittest ./my-chart
# Test file example
# tests/deployment_test.yaml
suite: test deployment
templates:
- deployment.yaml
tests:
- it: should render deployment
asserts:
- isKind:
of: Deployment
- equal:
path: spec.replicas
value: 3
- it: should set image from values
values:
- image:
repository: nginx
tag: "1.25"
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: nginx:1.25
# helm test (integration)
helm test my-release
helm test my-release --logs
# Validate with kubeconform
helm template my-release ./my-chart | kubeconform -strict
# Validate with kubectl
helm template my-release ./my-chart | kubectl apply --dry-run=client -f -
# Dry run with debug
helm install my-release ./my-chart --dry-run --debug
# CI/CD pipeline example
stages:
- lint
- unit-test
- validate
- deploy
lint:
script:
- helm lint ./my-chart --strict
unit-test:
script:
- helm unittest ./my-chart
validate:
script:
- helm template test ./my-chart | kubeconform -strict
deploy:
script:
- helm upgrade --install my-release ./my-chart --atomic --wait
Testing Best Practices:
- Lint charts before every commit
- Write unit tests for critical templates
- Define integration tests with helm test
- Validate manifests with kubeconform
- Use dry-run before deployment
- Automate testing in CI/CD
Production Deployment Best Practices
Use --atomic
Automatic rollback on failure. Ensures stable state after deployments.
Set --timeout
Set appropriate timeouts for large deployments. Default is 5 minutes.
Use --wait
Wait for resources to be ready before marking deployment successful.
Control History
Use --history-max to manage release history and storage.
GitOps Integration
Use ArgoCD or Flux for GitOps deployments. Git as source of truth.
Monitor Deployments
Monitor application health after deployment. Set up alerts and dashboards.
# Production deployment command
helm upgrade --install my-release ./my-chart \
--namespace production \
--create-namespace \
-f values-prod.yaml \
--atomic \
--wait \
--timeout 15m \
--history-max 20
# GitOps with ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/helm-charts
targetRevision: HEAD
path: charts/my-app
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# Monitoring after deployment
kubectl get pods -n production -l app=my-app
kubectl get svc -n production -l app=my-app
kubectl rollout status deployment/my-app -n production
# Rollback if needed
helm history my-release -n production
helm rollback my-release 2 -n production --wait
# Health checks
kubectl get events -n production --field-selector involvedObject.name=my-app
# Production checklist
# ✓ Tested in staging
# ✓ Values validated
# ✓ Secrets configured
# ✓ Backup in place
# ✓ Rollback plan ready
# ✓ Monitoring enabled
# ✓ Alerts configured
Production Deployment Checklist:
- Test in staging first
- Validate values and schema
- Configure secrets properly
- Back up release secrets
- Have a rollback plan
- Enable monitoring and alerts
- Use --atomic for automatic rollback
- Set appropriate timeouts
Helm Best Practices Summary
# Helm Best Practices Checklist
## Chart Structure
☐ Use standard directory structure
☐ Name charts consistently
☐ Document in README.md
☐ Use helpers for reusable code
☐ Keep templates focused
☐ Include values.schema.json
## Values Management
☐ Organize values logically
☐ Provide production-ready defaults
☐ Use environment-specific values files
☐ Never store secrets in values.yaml
☐ Use schema validation
☐ Use global values for shared config
## Versioning
☐ Follow semantic versioning
☐ Keep chart and app versions separate
☐ Pin dependency versions
☐ Tag releases in Git
☐ Maintain a changelog
☐ Limit release history
## Security
☐ Never store secrets in values.yaml
☐ Use external secret management
☐ Apply least privilege RBAC
☐ Sign and verify charts
☐ Scan for vulnerabilities
☐ Define security contexts
☐ Enforce policies with OPA
## Testing
☐ Lint charts before commit
☐ Write unit tests
☐ Define integration tests
☐ Validate manifests
☐ Use dry-run before deployment
☐ Automate testing in CI/CD
## Production Deployment
☐ Use --atomic for automatic rollback
☐ Set appropriate timeouts
☐ Use --wait for readiness
☐ Control history with --history-max
☐ Use GitOps for deployments
☐ Monitor deployments
☐ Have a rollback plan
Frequently Asked Questions
What is the most important Helm best practice?
Never store secrets in values.yaml. Use external secret management (Sealed Secrets, helm-secrets, External Secrets Operator) for all sensitive data.
How should I organize values for multiple environments?
Use a base values.yaml with defaults, then environment-specific files (values-dev.yaml, values-staging.yaml, values-prod.yaml) that override only what's needed.
What is the difference between chart version and appVersion?
Chart version refers to the Helm chart itself (changes when chart structure changes). appVersion refers to the application being deployed (changes when the app changes).
How do I test Helm charts properly?
Use a combination: helm lint for syntax, helm unittest for template rendering, helm test for integration, and kubeconform for Kubernetes API validation.
What flags should I use for production deployments?
Use --atomic (automatic rollback), --wait (wait for readiness), --timeout (sufficient timeout), and --history-max (manage history).
How do I secure my Helm charts?
Use external secret management, least privilege RBAC, chart signing, vulnerability scanning, security contexts, and policy enforcement with OPA/Gatekeeper.
Should I use Helm or Kustomize?
Both have their place. Use Helm for packaging and sharing applications. Use Kustomize for simple configuration overlays. Many teams use both together.
How do I implement GitOps with Helm?
Store charts in Git, use ArgoCD or Flux to sync them to clusters. Changes are made via Git commits, not manual Helm commands. Rollback is a Git revert.
Related Topics
Following Helm best practices ensures your charts are maintainable, secure, reusable, and production-ready. Start with the basics and gradually adopt more advanced practices as your charts mature.