Helm Best Practices
A field guide to shipping Helm charts and Kustomize overlays that stay reviewable, reproducible, and safe to upgrade.
How to Use This List
Treat each item as a check you can enforce in CI or in code review.
Groups A through E move from packaging discipline through security and delivery; adopt them in order if you are starting fresh.
None of these require exotic tooling - helm, kustomize, and kubectl cover all of them.
A - Versioning and Pinning
- Pin chart dependencies by exact or narrow range. Set
versioninChart.yamldependencies and commitChart.lockso builds are reproducible. - Bump the chart
versionon every chart change. GitOps controllers key on chart version to detect drift; a missed bump hides changes. - Keep
appVersionin sync with the image. Use.Chart.AppVersionas the default image tag so the packaged app and chart agree. - Never depend on a floating chart tag. Pin third-party charts by version, not
latest, or your cluster and CI will diverge. - Pin base images by digest. Reference application images by digest in production values so a moved tag cannot change what runs.
B - Values and Configuration
- Ship a
values.schema.json. Validate user-supplied values at install and upgrade so bad input fails fast with a clear message. - Keep the chart generic. Put environment-specific values in per-env files, not in the chart's default
values.yaml. - Document every value. Comment each key in
values.yamlso consumers know what is tunable and its default. - Prefer values files over long
--setchains. Files are reviewable and version-controlled; inline--setflags are easy to lose. - Use
globalvalues for cross-cutting settings. Share registry, image pull secrets, and labels across parent and subcharts via.Values.global.
C - Template and Overlay Hygiene
- Test rendered output in CI. Run
helm templateandkustomize buildon every PR and diff the result so surprises surface before merge. - Factor names and labels into helpers. Define them once in
_helpers.tplandincludethem so every object carries the recommendedapp.kubernetes.io/*labels. - Control whitespace deliberately. Use
{{-/-}}and pipeincludeoutput throughnindentto avoid malformed YAML. - Keep template logic shallow. If a template needs heavy conditionals and loops, reconsider whether an overlay would be clearer.
- Edit overlays, not bases, for env changes. Put every environment delta in its overlay so the base stays a single source of truth.
- Let generator hashes drive rollouts. Keep Kustomize
configMapGenerator/secretGeneratorname-suffix hashes enabled so config changes restart pods.
D - Security Defaults
- Run as non-root with a restricted context. Set
runAsNonRoot: true, drop capabilities, and useseccompProfile: RuntimeDefaultin templates. - Always set requests, limits, and probes. Bake resource requests, limits, and readiness/liveness probes into the chart so every install is schedulable and self-healing.
- Scan and sign chart images. Gate releases on a Trivy or Grype scan and verify Sigstore/cosign signatures before deploy.
- Ship default-deny NetworkPolicies. Include an opt-in NetworkPolicy so charts do not deploy fully open by default.
- Never template plaintext secrets into manifests. Source secrets from an external store or sealed/external secrets, not from committed values.
E - Delivery and Lifecycle
- Make upgrades idempotent. Use
helm upgrade --installin CI so the same command installs or upgrades safely. - Guard rollouts with
--atomicand--wait. Auto-rollback on failure and block until resources report ready. - Use hooks for migrations, with cleanup policies. Set
helm.sh/hookplushook-delete-policyso migration Jobs run at the right phase and do not accumulate. - Lint before packaging. Run
helm lintin CI and only thenhelm packageand push to your OCI registry. - Let one GitOps controller own apply and prune. Argo CD or Flux should render and reconcile; avoid manual
helm installagainst the same release.
When You Are Done
You should be able to render any environment offline, review a PR and see exactly what changes, and upgrade with automatic rollback on failure.
If a change to a ConfigMap does not roll pods, or a chart edit ships without a version bump, revisit groups A and C.
FAQs
Why test helm template in CI? It catches malformed YAML and unexpected rendered changes before they reach the cluster, without touching a live API server.
Pin by tag or digest? Tag for charts (semver version), digest for the underlying container images in production.
Where do environment values belong? In per-environment values or overlay files, never in the chart's default values.yaml.
How do I stop stale ConfigMaps? Keep Kustomize generator hash suffixes enabled, or roll the Deployment via a checksum annotation in Helm.
Should I use hooks for database migrations? Yes, a pre-upgrade hook Job with a hook-delete-policy is the standard pattern.
How do I make upgrades safe? Combine --install, --atomic, and --wait so failed upgrades roll back automatically.
Related
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).