Production engineering guide for helm chart best practices covering patterns, implementation strategies, and operational best practices.
Orientation This page collects concrete, repeatable practices for writing, maintaining, and troubleshooting Helm charts. It matters whenever you package Kubernetes applications as versioned, shareable units—whether you’re onboarding a new team, automating deployments across clusters, or auditing drift between environments. The following sections are organized by what you’re trying to accomplish, not by a file‑format taxonomy.
helm template renders a chart’s templates without contacting the Kubernetes API. It reads the chart directory, applies the default values.yaml, and prints the resulting manifests. This is the fastest way to verify hooks, resource names, and conditional logic before pushing to a cluster.
helm template myrelease ./mychart
If your environment has no kubeconfig, helm template still succeeds because it never talks to the API server. When a config exists but the context is unset, you’ll see Error: no configuration has been provided; exporting KUBECONFIG or running kubectl config current-context resolves it.
Override values inline:
helm template myrelease ./mychart --set replicaCount=3
Use --set-string when the value isn’t valid JSON:
helm template myrelease ./mychart --set-string image.tag=v1.2.3
Confused flags: --set expects a JSON‑compatible or comma‑separated value. --set-string always treats the right‑hand side as a literal string. Running --set image.tag=v1.2.3 works because v1.2.3 parses as a string, but --set image.tag="v1.2.3" embeds the quotes literally—prefer --set-string if you’re unsure. Conversely, --set-string will store a string even if the template expects an integer, causing a type mismatch on the next render.
helm lint scans a chart directory for best‑practice violations. Typical checks include:
apiVersion must be present in Chart.yaml and match v2 (current stable)..Values keys absent from values.yaml unless they have a default.annotations["helm.sh/hook"]; without hook-delete-policy, hook pods persist after helm uninstall.NOTES.txt is only valid if the chart intends to expose release notes.helm lint ./mychart
Sample output for a missing apiVersion:
Error: found in templates but not in charts: templates/hooks/job.yaml
And for an undefined value:
Error: template "deployment.yaml" has an undefined value at .Values.image.repository
Running lint in CI before chart publication catches these before they reach a cluster.
helm install --dry-run --debug renders templates and validates them against the cluster’s API without creating a release. This catches API‑level mismatches (e.g., a field valid in a template but rejected by the installed Kubernetes version) without persisting anything.
helm install --dry-run --debug myrelease ./mychart
Output ends with Status: failed or Status: pending depending on whether any objects would be created, and the debug log shows the full render + validation trace.
helm install myrelease ./mychart
This creates a release named myrelease and schedules the objects described by the chart. By default, helm install waits 300 seconds for all resources to become ready; adjust this with --timeout:
helm install myrelease ./mychart --timeout 600s
--timeout sets the wait duration in seconds. If the timeout expires before resources are healthy, the install fails and the partially created release stays in a failed state unless --atomic is also set, in which case the entire release is rolled back.
helm upgrade myrelease ./mychart
If the release doesn’t exist, helm upgrade behaves like helm install (the --install flag makes this explicit). During an upgrade, Helm performs a strategic merge of the new values against the release’s current values, then renders and applies the diff.
Value merge order: default values in values.yaml → values from the -f flag → values from --set flags → the release’s stored values. Command‑line --set overrides everything, including values from -f. This ordering is a frequent source of “I changed the values file but the release didn’t update” bugs.
helm upgrade myrelease ./mychart -f prod-values.yaml --set replicaCount=5
Here, replicaCount ends up as 5 regardless of what prod-values.yaml specifies.
Charts often define a strategy block in the deployment template. Helm passes this through; the default is RollingUpdate. For a Recreate strategy, ensure your template specifies it:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
strategy:
type: Recreate
replicas: {{ .Values.replicaCount }}
...
helm rollback myrelease 3
This reverts the release to revision 3, re‑rendering templates from that revision’s stored values. Rollback is safe only if the target revision was successful; if it failed, Helm returns Error: release "myrelease" has no revision 3 or Error: cannot rollback to a failed revision.
List revisions:
helm history myrelease
Sample output:
REVISION UPDATED STATUS CHART DESCRIPTION
1 2024-01-15 10:23:43.123 superseded mychart-0.1.0 first release
2 2024-01-16 08:11:09.456 failed mychart-0.1.1 attempted upgrade, hook failed
Most charts rely on external charts—databases, monitoring stacks, ingress controllers. Helm handles these via the requirements.yaml (or the legacy dependencies field in Chart.yaml).
helm dependency update
This reads Chart.yaml (or requirements.yaml), resolves the specified versions, downloads the referenced charts into charts/, and generates or updates Chart.lock. The lock file ensures reproducible builds: anyone running helm dependency build later will install exactly the versions recorded in the lock, not re‑resolve from the remote index.
If you only need to fetch a chart without updating the version, use helm dependency build:
helm dependency build
This reads the existing Chart.lock and populates charts/ without contacting remote repos. Running helm dependency build on a repo without a lock file produces Error: no lock file found, run dependency update first.
In Chart.yaml, each dependency entry specifies a version range:
dependencies:
- name: prometheus-kube-state-metrics
repository: oci://ghcr.io/prometheus/kube-state-metrics
version: "5.x"
The version field supports semver ranges (~, >=, <), but pinning to an exact version or a narrow range prevents silent upgrades when the repository maintainer publishes a new major version. After changing versions, always run helm dependency update to regenerate the lock.
Modern Helm charts can pull dependencies from OCI registries:
helm dependency add oci://ghcr.io/example/chart --version 1.2.0
This writes the dependency entry to Chart.yaml and fetches the blob into charts/. OCI URLs replace the older repository: format that pointed to Helm v2‑style chart repositories. The standalone helm pull oci://... command also works for pulling a single chart directly:
helm pull oci://ghcr.io/example/chart --version 1.2.0
This saves the chart archive to the current directory. OCI pull requires appropriate registry credentials; if your dockerconfigjson doesn’t include the registry, you’ll get Error: unauthorized: authentication required.
helm lint runs a set of checks defined in Helm’s source. Key rules include:
apiVersion must be present in Chart.yaml and match v2.templates/ directory contains a file named NOTES.txt, its content is documented as the release notes..Values key that is absent from values.yaml unless it has a default definition.annotations["helm.sh/hook"]; unless hook-delete-policy is set, the hook pods will persist after the release is uninstalled.Running helm lint before merging chart changes prevents the most common publishing rejections.
Helm supports tests that run after a release is installed. Define them in tests/tests.yaml:
apiVersion: v1
kind: Test
metadata:
name: mychart-test
spec:
template:
spec:
containers:
- name: curl
image: curlimages/curl:8.0.0
command: ["curl", "-fsS", "{{ .Values.healthCheckEndpoint }}"]
restartPolicy: Never
Apply tests during install:
helm install myrelease ./mychart --set test.enabled=true
Or run them manually against an existing release:
helm test myrelease
helm test executes each test container, captures exit code, and reports pass/fail. If a test times out (default 60 seconds), it fails with Error: test "mychart-test" timed out after 60s. Adjust the timeout with --timeout on helm test.
Common test patterns
200 OK.kubectl get pods inside the test container and assert on output.Keep test images small and stateless; avoid tests that depend on cluster‑scoped resources unless your chart installs them.
A common surprise is the hook-delete-policy annotation. If you omit it, the hook pod is not garbage‑collected when helm uninstall runs, leaving a defunct pod in the namespace:
apiVersion: v1
kind: Job
metadata:
name: {{ include "mychart.fullname" . }}-backup
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-delete-policy": hook-succeeded,hook-failed
Without that annotation, helm uninstall myrelease completes but you’ll see the backup job still running with kubectl get jobs. This silently accumulates resources across releases.
Another hook gotcha: --atomic combined with any pre/post hook that fails causes the entire release to be rolled back immediately. If you’re debugging a flaky hook, running without --atomic lets the release proceed while the hook logs its output to helm install --debug output.
--set vs --set-string gotchasAs noted earlier, --set interprets its argument as JSON. Passing a value that looks like a number but Helm’s parser treats differently can produce an unexpected type in the rendered template. --set-string always sets a string value, even if the template expects an integer. Mixing them in the same release upgrade can produce a chart that renders once but fails on the next upgrade after a values‑file change re‑merges types.
Inspect what a --set flag produced:
helm get values myrelease
This prints the effective values stored with the release, showing exactly how --set was absorbed during the last install or upgrade.
helm template does not require a running Kubernetes API server. If you run it on a machine with no kubeconfig, it still outputs rendered manifests. If you do have a config but point it at a cluster where the chart’s API groups aren’t installed (e.g., a minimal kind cluster without metrics-server), helm install --dry-run --debug may succeed in rendering but fail at the API validation step with:
Error: unable to recognize "v1beta1" kind "Pod" in version "v1": no matches for kind "Pod" in group ""
This isn’t a chart error; it’s a cluster‑version mismatch. Always validate renders on a cluster that matches your target version, or use helm template locally and apply the output manually.
Helm’s .Values helpers return nil when a key is absent from values.yaml and has no default. In templates, {{ .Values.replicaCount }} prints nothing (empty string), while {{ .Values.replicaCount | default 1 }} substitutes 1. A frequent bug is writing replicas: {{ .Values.replicaCount }} and wondering why the deployment shows 0 replicas after a fresh install—the default was never set. Always pair potentially‑missing values with | default or declare them in values.yaml.
When you run helm upgrade --install with multiple -f flags, Helm merges them in order, then applies --set on top. If two values files define the same key, the last one wins. This is easy to miss in CI pipelines where helm upgrade -f staging.yaml -f prod.yaml might have the files in reverse order of intent. To avoid confusion, prefer a single merged values file for production, or use --set only for override flags that must differ per environment.
helm upgrade myrelease ./mychart -f env.yaml -f overrides.yaml
If overrides.yaml defines replicaCount: 2 and env.yaml defines replicaCount: 3, the release ends up with 2. Reverse the order and you get 3.
helm repo add and index stalenessAdding a repo with helm repo add downloads the repository’s index.yaml into ~/.helm/repos/. That index is not refreshed automatically. Running helm search repo or helm install against a stale repo may return outdated chart versions or, worse, fail to find a version you just pushed. The habit is:
helm repo update
before any search or install that depends on the latest index. Forgetting this is a silent source of “chart not found” errors in scripts.
helm pull oci:// and credential cachinghelm pull oci://ghcr.io/example/chart relies on the same credential helpers as docker pull. If you’re using a corporate proxy or an SSH‑key‑based registry, the first pull may prompt for credentials and cache them in ~/.docker/config.json. Subsequent runs in a different shell or CI container without the same volume mounts will fail with Error: unauthorized: authentication required. To avoid this, either mount the docker config volume, use an auth token via HELM_OCI_AUTH, or fall back to HTTP‑based chart repositories where basic auth can be passed on the command line.
| Flag | Effect | Common Misstep |
|---|---|---|
--timeout | Seconds to wait for resource readiness | Setting too short causes premature failure; too long blocks CI pipelines |
--atomic | Abort and rollback on any hook failure | Using it during development makes debugging hook failures harder |
--dry-run | Render + API validate without persisting | Omitting --debug hides the rendered templates when validation fails |
--set | JSON/comma‑separated inline values | Forgetting that --set overrides -f values, even those from later flags |
--set-string | Literal string values | Using it for numeric or boolean templates causes type mismatches |
This reference page concludes the core patterns, commands, and failure modes you’ll encounter when working with Helm charts. Keep the sharp‑edges checklist handy; the majority of production incidents stem from the quiet gotchas listed above.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy