Who this is for
You are copying the same Deployment and Service YAML between environments, changing three values each time. You do not need to write a chart to use one.
What a chart is
mychart/
Chart.yaml # name, version, appVersion
values.yaml # the defaults someone can override
templates/
deployment.yaml
service.yaml
_helpers.tpl # named templates, no output of its own
charts/ # dependencies, vendored
# Chart.yaml
apiVersion: v2
name: mychart
version: 1.4.2 # the chart's own version
appVersion: "2.1.0" # the application it installs
Those two version fields are different and both matter. version changes when
you change the chart; appVersion changes when the software changes. Bumping
only appVersion after editing a template is how a repository ends up with two
different charts claiming to be 1.4.2.
Templates
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
resources:
{{- toYaml .Values.resources | nindent 12 }}
Three mechanics that account for most template debugging:
{{- trims preceding whitespace. The blank lines a conditional leaves behind are harmless, but YAML is
indentation-sensitive, so a line at the wrong depth — or two lines joined because a
{{- ate the newline between them — is a parse error rather than a cosmetic problem.
nindent indents the whole block it emits. toYaml .Values.resources produces
multiple lines, and every one of them needs the indentation — indent pads
every line, the first included, but the first line lands wherever the tag sits —
usually after a key on the same line, so the padding ends up mid-line; nindent adds
a newline first, so every line starts where it should. Using the wrong one is usually
a parse error, not a cosmetic one.
required makes a missing value a build error rather than an empty string:
host: {{ required "ingress.host must be set" .Values.ingress.host }}
Without it, a typo'd value renders as nothing, and you deploy an Ingress with an empty host that matches everything.
Values, and which one wins
helm upgrade --install web ./mychart \
-f values-production.yaml \
--set image.tag=2.1.4
Precedence, lowest to highest: the chart's values.yaml, then each -f file
in the order given, then --set. Later wins, and merging is per-key rather
than per-file — so a -f file only needs the keys it changes.
One trap: lists are replaced, not merged. Overriding a single entry of a list means restating the whole list.
The commands that matter
helm template web ./mychart -f values-production.yaml # render locally, apply nothing
helm upgrade --install web ./mychart -f values-production.yaml --rollback-on-failure --timeout 5m
helm diff upgrade web ./mychart -f values-production.yaml # plugin, well worth installing
helm history web
helm rollback web 3
helm template is the one to reach for first. It renders to stdout with no
cluster involved, which turns "why did it deploy that" into something you can
read. Pipe it through the manifest analyzer to check
what you are about to apply.
upgrade --install is idempotent: install if absent, upgrade if present. It is
what belongs in a pipeline, rather than branching on whether the release exists.
--rollback-on-failure (called --atomic before Helm 4, which still accepts the old
name with a deprecation warning) is the flag most worth adopting. It waits for
resources to become ready and rolls back automatically if they do not, so a
failed upgrade leaves the previous working release running instead of a half-applied
one. It turns on --wait, so always give it a --timeout.
Upgrading to Helm 4
Added 29 October 2026. --rollback-on-failure above is a Helm 4 name, but
the upgrade changes more than names. Checked against Helm's issues and release
notes up to 4.3.1:
- Null overrides can stop working. Setting a subchart key to
nullin your values is Helm's documented way to delete a subchart default. From 4.2.0 that null is stripped when no-for--setvalue touches the subchart's scope, so the default comes back. It was still open in 4.3.1 when checked on 21 October. - Empty entries vanish. A key written as
one:with nothing after it is removed from.Valuesfrom 4.2.0, so arangeover that map skips it. Writingone: {}should keep it (read in Helm's source, not run). --waitcan hang on custom resources such as Argo Rollouts, until the timeout.--wait=legacyuses Helm 3's polling.--rollback-on-failureturns waiting on, so it is affected too.- Two loud changes:
--post-renderertakes the name of a plugin, not a script, andhelm registry logintakes the host name only.
Before switching, render the chart with both versions and compare
(helm3 and helm4 here stand for the two binaries):
helm3 template web ./mychart -f values-production.yaml > rendered-v3.yaml
helm4 template web ./mychart -f values-production.yaml > rendered-v4.yaml
diff -u rendered-v3.yaml rendered-v4.yaml
Every item, marked as documented or as a regression with its issue, is on Helm 3 to Helm 4.
Where the release record bites
Helm stores each release's state in a Secret in the release namespace. That is
how history and rollback work — and it is why two surprising things happen.
"cannot re-use a name that is still in use" means a release record exists,
even if its resources are gone. helm list -a shows it; helm uninstall
clears it.
A release stuck pending-upgrade happens when a helm upgrade is
interrupted — CI cancelled, laptop closed — leaving the record mid-transition.
Nothing will proceed until it is resolved: roll back to the last good revision,
and avoid deleting the Secret by hand unless you understand what you are
discarding.
Resources Helm did not create are not managed. If somebody kubectl apply'd something first, Helm will refuse to adopt it rather than overwrite
it.
Common mistakes
--setfor secrets. It goes into shell history, into CI logs, and into the release record. A-ffile keeps it out of history and logs but not out of the release record, which stores every value you pass. Keep secrets out of Helm values entirely and have the chart reference a Secret that something else manages.- Editing a Helm-managed resource with
kubectl. The next upgrade restores the chart's version, so the fix disappears without explanation. - No resource requests or limits in
values.yaml. The chart installs fine, then behaves unpredictably under load — see requests vs limits. :latestas the image tag. Nothing in the rendered manifest changes between releases, sohelm rollbackrolls back to the same image.- Trusting
helm lintas validation. It checks the chart is well formed, not that the rendered Kubernetes objects are correct.
If this did not work
- "parse error … did not find expected key". Rendering produced invalid
YAML.
helm templatethe chart and read the output around that line — it is nearly alwaysindentwherenindentwas needed. - The release says deployed and nothing is running. Helm reports on
applying manifests, not on the workload being healthy. Without
--waitor--rollback-on-failureit will happily report success for a Deployment that cannot pull its image — ImagePullBackOff. - A value in
values.yamlhas no effect. It is probably being overridden by a-ffile or--set;helm get values webshows the overrides the release was given, andhelm get values web --allthe full set it actually used. - A subchart ignores your value. Subchart values are namespaced under the
subchart's name:
postgresql.auth.password, notauth.password.
Next steps
Take one application you deploy with raw YAML and run
helm create to scaffold a chart around it, then helm template it and diff
the output against your existing manifests. The differences are the defaults
the scaffold assumed, and reading them is a fast way to learn what charts
normally parameterise.
Related on this site
- requests vs limits — The two values in every chart that decide scheduling and eviction.
- Manifest analyzer — Check what a chart actually renders.
- ImagePullBackOff — The most common failure after a chart installs cleanly.
Mentioned in this article
- yaml: did not find expected keyProblem · YAML
Tagged with
Enjoyed this article?
Get more DevOps insights delivered to your inbox.
Get new posts by email
Subscribe to get an email when a new blog post is published. Skip anytime.
No spam, unsubscribe anytime.
Related Posts
Discussion
0 comments
Sign in to join the conversation.
Be the first to comment
Start a conversation about this post



