Helm Helm

One Chart, Many Environments: Values Files for Dev, Staging, and Prod

Override just what differs between environments, and understand the order values are merged in.

10 min Intermediate

A chart’s values.yaml holds sensible defaults, but dev, staging, and production rarely share a namespace, a replica count, or a config value. This guide covers layering additional values files per environment, without maintaining separate copies of the chart itself. It builds on the webapp chart from the previous guide.

Keep values.yaml as the shared baseline

Leave anything that should be identical everywhere, the app name, the image repository, in values.yaml. Only environment-specific fields belong in an override file:

# values-dev.yaml
namespace: dev
config:
  customHeader: "welcome to dev"
# values-prod.yaml
namespace: prod
replicaCount: 5
config:
  customHeader: "welcome to production"

A file passed with -f doesn’t replace values.yaml, it’s merged on top of it. values-dev.yaml only needs to mention namespace and config.customHeader; everything else, appName, image, falls through from the base file unchanged.

Reference the namespace in your templates

Add the value and use it wherever a manifest needs to declare its namespace:

# values.yaml (add a default)
namespace: default
# templates/deployment.yaml, templates/service.yaml, templates/configmap.yaml
metadata:
  name: {{ .Values.appName }}
  namespace: {{ .Values.namespace }}

Install with an environment’s values file

kubectl create namespace dev
kubectl create namespace prod

helm install web-dev ./webapp -f values-dev.yaml -n dev
helm install web-prod ./webapp -f values-prod.yaml -n prod

Two things are doing related but distinct jobs here: -n dev tells Helm which namespace to store the release’s own metadata in, and metadata.namespace inside the templates tells Kubernetes which namespace each resource goes into. Keeping them the same, the release lives in the same namespace as what it deployed, is standard practice and avoids confusion later when you run helm list -n dev and expect to see everything that’s actually running there.

helm list -A
kubectl get all -n dev
kubectl get all -n prod
curl <dev-pod-ip>   # or kubectl port-forward, per NOTES.txt

Confirm each environment shows its own customHeader and replica count.

The full precedence order

Helm merges values from several sources, each overriding the ones before it:

  1. values.yaml, the chart’s own defaults.
  2. A parent chart’s values.yaml, if this chart is used as a dependency (not covered here).
  3. Any file passed with -f, in the order given, later files win over earlier ones.
  4. --set, --set-string, or --set-json flags, which win over every file.
helm upgrade web-dev ./webapp -f values-dev.yaml --set replicaCount=1

--set is convenient for a one-off override or a quick test, but for anything you want to reproduce, put it in a file and check it into version control. A --set flag typed at a terminal leaves no record of what was actually deployed.

Two ways to check what a merge produced

Before trusting an install or upgrade, look at the result:

helm template ./webapp -f values-prod.yaml
helm get values web-prod

helm template -f renders the chart with a given values file exactly as install would, without touching the cluster, so you can diff two environments’ output directly. helm get values (after install) shows the values actually recorded against a live release, which is what to check when a running release doesn’t match what you expect a file to produce.

A note on secrets

Values files are plain text, so don’t put real credentials in one committed to Git. A common split is: non-sensitive per-environment config in values-<env>.yaml, and secrets injected either via --set from a CI secret store, via a tool like Sealed Secrets or SOPS, or by having the chart reference a Kubernetes Secret created outside of Helm rather than embedding the value in values.yaml at all.

Popular Guides