Values are the interface between users and charts: the precedence hierarchy from default values.yaml, -f files, to --set on the CLI, how to set values, values file structure for complex data, and per-environment configuration patterns like values-dev.yaml and values-prod.yaml.

After episode 5, where we covered upgrade, rollback, and release history — how changes are safely applied to a running release — in this episode we step back half a pace to dissect the main fuel of every Helm operation: values.
Why does this topic matter? Imagine a chart as an application with many settings, and values as its control panel — all the buttons and dials that determine application behavior. In the real world, most of a DevOps/Platform engineer's daily Helm work isn't writing Go templates, but crafting the right values file for a given environment: how many replicas for production, which image tag to deploy to staging, how much CPU to allocate to service X. The most common production errors aren't broken templates, but wrong values — a wrong image tag, an overridden secret, or resource limits that get Pods evicted. Understanding the precedence hierarchy and how values merging works is what separates "a helm install that happens to work" from "helm installs you can manage across hundreds of releases."
In this episode we'll dissect the values precedence hierarchy, the various ways to set values from the CLI, the merge rules for multiple files, values file structure for complex data types, examples of common configuration, and most importantly: the per-environment values strategy used by production teams.
Helm determines a parameter's final value from three layers, from lowest to highest priority:
| Priority | Source | When It's Used |
|---|---|---|
| Low | Chart's built-in values.yaml | Defaults defined by the chart author; always loaded |
| Medium | User values files (-f / --values) | Configuration from you; can be more than one file |
| High | --set, --set-string, --set-file flags | Quick overrides straight from CLI/CI |
The rule of thumb: the more specific, the more it wins. --set beats files, files beat chart defaults. The analogy is adjusting the AC in a building: values.yaml is the default temperature of the whole building, values files are per-floor settings, and --set is the manual knob you turn right now for this room.
helm upgrade --install frontend bitnami/nginx \
--namespace web \
--values values.yaml \
--set image.tag=1.27.0In the example above, image.tag from --set overrides the image.tag in values.yaml, which overrides the default in the chart's built-in values.yaml. Parameters not touched at all fall to the lowest available value.
Note
Merging is a deep merge for maps: values at the top level are preserved if not overridden at a lower level. But for lists/arrays, there's no merging — a list from a higher-priority source replaces the entire list from a lower-priority source. This is a frequent source of bugs: changing one item in an args list via --set will delete the other items defined in the values file. Always be aware of the data type you're overriding.
--set, --set-string, --set-fileThese three flags are how Helm accepts values directly from the command line — the main weapon in CI/CD pipelines.
--set follows dot-based path syntax and can combine several values at once with commas:
helm upgrade --install frontend bitnami/nginx \
--set image.tag=1.27.0,replicaCount=3 \
--set resources.requests.cpu=250m,resources.requests.memory=512MiNumeric-looking values (like 3 and 250m) and true/false will be interpreted as their data types, not strings. Lists can be written with curly braces: --set providers={gcp,aws,azure} produces a list of three items. Values containing commas or special characters need escaping, and for keys with a literal dot, use a backslash: --set nodeSelector."kubernetes\.io/os"=linux.
--set-string forces every value to be treated as a string, whatever its form. This matters for values that look numeric but must be strings — for example, a version code like 1.0 that would lose its trailing zero if parsed as a number, or a phone number.
helm upgrade --install frontend bitnami/nginx \
--set-string app.version=1.0 \
--set-string http.cors.origins="https://a.com,https://b.com"--set-file reads the contents of a file as a value — usually to embed file content (for example, an nginx.conf configuration or a public key) directly into a template rendered as a multiline string:
helm upgrade --install frontend bitnami/nginx \
--set-file nginx.conf=/etc/nginx/custom.confTip
Be careful how --set handles values with special characters like $, [, ], *, and ". In a shell, those values can be interpolated or expanded before Helm sees them. Always wrap values in quotes (--set 'key=$VALUE'), and for truly unruly values, it's safer to put them in an -f file than to craft escapes on the CLI.
Helm accepts multiple -f files, and files listed later win over files listed earlier. The chart defaults are always the bottom layer, below all files:
helm upgrade --install frontend bitnami/nginx \
--namespace web \
--values values-common.yaml \
--values values-web.yaml \
--values secrets-placeholder.yamlThe reading flow: rendering starts from the chart's values.yaml → overwritten by values-common.yaml → overwritten again by values-web.yaml → overwritten by secrets-placeholder.yaml. Deep merge rules apply: if values-common.yaml sets replicaCount: 2 and values-web.yaml sets replicaCount: 5, the final result is 5 — but if values-web.yaml only sets image.tag, the replicaCount value from values-common.yaml is preserved.
The "base + override" pattern is the foundation of the environment strategy we'll discuss shortly. Imagine cooking with a base recipe and adjusting the seasoning per menu: the first file holds ingredients shared by every menu, subsequent files only hold the differences for a specific menu.
Values files are ordinary YAML, and because YAML supports nested structures, values can hold very expressive configuration:
replicaCount: 3
image:
repository: ghcr.io/company/frontend
tag: "1.27.0"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
service:
type: ClusterIP
port: 80
annotations:
prometheus.io/scrape: "true"
ingress:
enabled: true
hosts:
- host: app.company.com
paths:
- path: /
pathType: Prefix
env:
LOG_LEVEL: info
ALLOWED_ORIGINS:
- https://web.company.com
- https://api.company.com
labels:
team: platform
cost-center: coreNote the types that appear: nested maps (resources.requests.cpu), lists of maps (ingress.hosts with elements containing host and paths), lists of strings (env.ALLOWED_ORIGINS), and deliberate booleans/strings — the value tag: "1.27.0" is quoted so it isn't interpreted as a number. This type consistency matters because templates access these with .Values.<path> and type errors are one of the most common causes of helm template failing at render.
Important
The golden rule of values structure: values that depend on the environment must be data, not logic. Don't put "ifs" inside values; values must be purely declarative. If you feel the need for global:, make sure it's really needed — global values propagate to every subchart and can make configuration hard to trace. The global details will be covered when we discuss dependencies in episode 11.
Even though every chart differs, there are five groups of configuration that are almost always set when deploying a production application:
pullPolicy. Make sure tag is a string, and for production prefer pullPolicy: IfNotPresent (the image is already pulled locally) or Always for frequently changing commit hashes.ClusterIP (internal), NodePort (accessible from outside via node ports), LoadBalancer (cloud LB), or ClusterIP + Ingress (the most common pattern).All of these are just an interface — a good chart documents them in the README and its built-in values.yaml with comments. Your job as a chart user is to pick the right value per environment.
values-dev, values-staging, values-prodThe chart is one, the environments are many. The same values for production (replica 6, memory 2Gi, host app.company.com) clearly don't fit development (replica 1, memory 256Mi, host app-dev.company.com). The industry standard pattern is to separate those differences into per-environment values files:
replicaCount: 1
image:
tag: "dev"
pullPolicy: Always
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 250m
memory: 512Mi
ingress:
enabled: true
hosts:
- host: app-dev.company.comUsing them in CI/CD is simple: helm upgrade --install frontend ... --values values-prod.yaml. Note that the per-environment file only contains differences — values shared by every environment are best placed in the chart's built-in values.yaml or in a values-common.yaml loaded first.
Some organizational strategies used by production teams:
values-dev.yaml, values-staging.yaml, values-prod.yaml, plus values-common.yaml as the base. Simple, transparent, and reviewable in a single PR.--set in the pipeline. Values that are truly build-specific (the CI image tag, git commit SHA) are set directly, while static configuration goes in files. Example: --set image.tag=$CI_COMMIT_SHA.frontend-prod, frontend-staging) — the same chart, the same release pattern, different values.Warning
The biggest danger of the per-environment pattern is environment drift: production and staging "never look the same," so bugs that pass staging show up in production. The only antidote is forcing staging to run a configuration as close to production as possible — starting with using the same values except those that genuinely must differ (host, resources, replica count), and deploying staging from the same image as production.
After understanding how to set values, there's one discipline that's often skipped: verifying what actually applies. Because precedence can obscure where a value came from, never guess the final result — ask the release directly:
helm get values frontend --namespace web
helm get values frontend --namespace web --allThe first command only shows values that differ from the chart defaults (those set via -f/--set); the --all option shows the full effective values after merging — this is the "final truth" that templates will render. To complete the audit, helm get manifest frontend --namespace web shows the rendered manifests that were actually sent to the cluster.
When does this matter? Two critical moments. First, after an upgrade — to make sure the override you thought succeeded wasn't overwritten by another file listed later. Second, when handing a release to another engineer: before touching someone else's release, read its helm get values first so you don't trample values deliberately set earlier. This is the same audit habit as reading a diff before merging — cheap, but it saves many incidents.
Tip
When arguing "which value applies," helm get values <release> --all is the final arbiter — not memory, not re-reading command history. For output you can grep or parse with other tools, add -o yaml or -o json: helm get values frontend -o json.
In episode 6 we've dissected how values are a configuration layer determined by a precedence hierarchy: the chart's built-in values.yaml as the base, -f files as medium overrides, and --set/--set-string/--set-file as the highest overrides. We learned the merge rules — deep merge for maps, total replacement for lists — how to set values safely from the CLI, values file structure for nested YAML and complex data, the five common configuration groups (image, resources, replica, service type, ingress host), and the values-dev.yaml, values-staging.yaml, and values-prod.yaml strategy with a base + override pattern.
Key takeaways:
--set.--set-string for values that must be strings; --set-file to embed file contents.Now you understand how to control charts from the outside. In the next episode, episode 7, we flip the perspective — from chart user to chart author: creating your first chart. We'll dissect helm create, the generated directory structure, the Chart.yaml file as metadata, designing good values.yaml, basic Deployment/Service/ConfigMap templates, and how to test a chart with helm lint, helm template, and helm install --dry-run. See you in episode 7!