Multi-Environment Values
Managing dev, staging, and production from a single Helm chart
The Multi-Environment Challenge
Every application eventually runs in more than one environment. Development needs hot reloading and verbose logging. Staging mirrors production topology at smaller scale. Production demands high availability, strict resource limits, and encrypted secrets. The question is not whether you need different configurations — it is how you manage them without duplicating your entire chart for each environment.
Helm solves this through values layering: a single set of templates combined with environment-specific values files. The chart itself never changes between environments. Only the values that feed into it differ. This principle — one chart, many values files — is the foundation of multi-environment Helm management.
Values Layering with -f Flags
When you pass multiple -f (or --values) flags to helm install or helm upgrade, Helm deep-merges them from left to right. Values in later files override those in earlier files at every key level.
helm upgrade myapp ./mychart \
-f values/base.yaml \
-f values/prod.yaml \
--set image.tag="v2.4.1"
The merge order here is: chart values.yaml (defaults) → base.yaml → prod.yaml → --set flags. Each layer can override any key from the previous layer.
Merge Semantics
Understanding how Helm merges values is critical to avoiding surprises:
- Scalar values (strings, numbers, booleans) — replaced entirely by the later file
- Maps / objects — deep-merged recursively. Keys from the later file are added or overridden; keys not mentioned are preserved from the earlier file
- Lists / arrays — replaced entirely (not appended). If
prod.yamldefinesingress.hosts, the entire list frombase.yamlis discarded
What Changes Per Environment
While every project differs, these categories cover the vast majority of environment-specific values:
| Category | Dev | Staging | Prod |
|---|---|---|---|
| Replica count | 1 | 2 | 3–10+ |
| CPU / memory requests | Low | Medium | Right-sized |
| Image tag | latest or branch | RC tag | Pinned release |
| Ingress host | dev.internal | staging.example.com | app.example.com |
| TLS | Disabled or self-signed | Let's Encrypt staging | Let's Encrypt prod |
| Feature flags | All on | Selective | Stable only |
| Log level | debug | info | warn |
| External endpoints | Mock / local | Staging services | Production services |
Keep your base values file as the "dev-friendly default" so developers can helm install with zero overrides and get a working local deployment.
Directory Structure Patterns
Flat Pattern
For simple projects with two or three environments, store values files alongside the chart:
mychart/
Chart.yaml
values.yaml # defaults (doubles as dev)
values-staging.yaml
values-prod.yaml
templates/
Deploy with: helm upgrade myapp . -f values-prod.yaml
Layered Pattern
For teams managing multiple services across regions and clusters, separate the chart from the deployment configuration:
deploy/
charts/
mychart/ # the chart itself
values/
base.yaml # shared across all envs
envs/
dev.yaml
staging.yaml
prod.yaml
regions/
us-east.yaml
eu-west.yaml
secrets/
prod-secrets.enc.yaml
helmfile.yaml
Deploy with: helm upgrade myapp charts/mychart -f values/base.yaml -f values/envs/prod.yaml -f values/regions/us-east.yaml
Helmfile: Declarative Multi-Environment Management
Helmfile is a declarative wrapper around Helm that manages multiple releases across environments from a single YAML file. Instead of writing shell scripts that call helm upgrade with the right flags, you declare what you want and let Helmfile figure out the commands.
Core Structure
# helmfile.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
environments:
dev:
values:
- values/envs/dev.yaml
staging:
values:
- values/envs/staging.yaml
prod:
values:
- values/envs/prod.yaml
releases:
- name: myapp
chart: ./charts/mychart
namespace: {{ .Values.namespace }}
values:
- values/base.yaml
- values/envs/{{ .Environment.Name }}.yaml
set:
- name: image.tag
value: {{ .Values.imageTag | default "latest" }}
The environments block defines named environments with their own values. The releases block lists every Helm release. Helmfile supports Go templating (via {{ }}) so you can reference environment names and values dynamically.
Key Commands
helmfile -e prod sync— install or upgrade all releases for theprodenvironmenthelmfile -e prod diff— show what would change without applying (requires the helm-diff plugin)helmfile -e prod apply— run diff first, then sync only if there are actual changeshelmfile -e prod template— render all manifests to stdout for reviewhelmfile -e prod destroy— uninstall all releases for the environment
helmfile -e $ENV apply is often the only deploy command you need. The environment name comes from the pipeline variable (e.g., the Git branch or a manual trigger).
Secret Management Strategies
Secrets are the hardest part of multi-environment configuration. They must differ per environment, must never appear in Git as plaintext, and must be available at deploy time. Three patterns dominate:
1. CI Variable Injection
The simplest approach: store secrets in your CI system (GitHub Actions secrets, GitLab CI variables, etc.) and inject them at deploy time with --set:
helm upgrade myapp ./mychart \
-f values/base.yaml \
-f values/prod.yaml \
--set secrets.dbPassword="${DB_PASSWORD}" \
--set secrets.apiKey="${API_KEY}"
Pros: no extra tooling, secrets never touch Git. Cons: does not scale well beyond a handful of secrets; difficult to audit.
2. SOPS + helm-secrets Plugin
SOPS (Secrets OPerationS) encrypts YAML values files so they can be committed to Git safely. The helm-secrets plugin decrypts them transparently during helm install/upgrade:
# Encrypt with age (modern, simple key management)
sops --encrypt --age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
values/prod-secrets.yaml > values/prod-secrets.enc.yaml
# Deploy with transparent decryption
helm secrets upgrade myapp ./mychart \
-f values/base.yaml \
-f values/prod.yaml \
-f values/prod-secrets.enc.yaml
SOPS supports age, PGP, AWS KMS, GCP KMS, and Azure Key Vault as key backends. The encrypted file lives in Git; only the decryption key stays in your CI environment or developer keyring.
3. vals and External Secret Operators
For teams already using a secrets manager (Vault, AWS Secrets Manager, etc.), the vals tool or Kubernetes External Secrets Operator can pull secrets directly at deploy time without storing them in values files at all.
Kustomize + Helm: A Comparison
Kustomize and Helm both solve the multi-environment problem, but from opposite directions. Helm uses templating: you write parameterized templates and fill in values. Kustomize uses patching: you write complete YAML manifests and overlay patches per environment.
In practice, many teams combine both. helm template renders a chart into raw YAML, and Kustomize overlays apply cluster-specific patches (like injecting a sidecar or changing a namespace annotation). ArgoCD supports this "Helm + Kustomize" pattern natively.
Choose Helm values layering when your chart is a reusable package deployed across teams. Choose Kustomize when you need surgical patches to third-party manifests you do not control.
The CI Pipeline Pattern
The recommended production workflow ties everything together:
- Build — CI builds and tags the container image (e.g.,
v2.4.1) - Render —
helmfile -e $ENV templaterenders manifests for review - Diff —
helmfile -e $ENV diffshows what will change in the cluster - Apply —
helmfile -e $ENV apply --set image.tag=$TAGdeploys - Verify — smoke tests run against the deployed environment
Each environment has its own pipeline stage or trigger. Dev deploys on every push, staging on merge to main, production on manual approval or tag creation. The chart artifact is identical across all environments — only the values file changes.