Helm values-per-environment: the -f layering pattern
Published: 2026-03-12
Helm's -f flag lets you layer values files. The last file wins for any given key. This is the basis for environment-specific configuration without duplicating your entire values.yaml for each environment.
The pattern
charts/my-service/
Chart.yaml
values.yaml # defaults (full config, production-safe)
values-dev.yaml # dev overrides only
values-test.yaml # test overrides only
values-prod.yaml # prod overrides (minimal — most keys match defaults)
Deploy with:
bashhelm upgrade --install my-service ./charts/my-service \
-f charts/my-service/values.yaml \
-f charts/my-service/values-dev.yaml \
--namespace dev
values.yaml is the base. values-dev.yaml overrides only what's different. The environment file is typically 5–20 lines, not a copy of the full config.
values.yaml: production defaults
yamlreplicaCount: 3
image:
repository: registry.example.com/my-service
tag: "latest"
pullPolicy: Always
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
ingress:
host: my-service.example.com
env:
LOG_LEVEL: "warn"
FEATURE_FLAG_NEW_UI: "false"
DB_POOL_SIZE: "10"
values-dev.yaml: minimal overrides
yamlreplicaCount: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: false
ingress:
host: my-service.dev.example.com
env:
LOG_LEVEL: "debug"
FEATURE_FLAG_NEW_UI: "true"
DB_POOL_SIZE: "2"
Only keys that differ from production are specified. Everything else inherits from values.yaml. If a new config key is added to the base, all environments automatically get the production-safe default.
FluxCD HelmRelease with per-env values
In a FluxCD GitOps setup, the cleanest pattern is inline overrides in the HelmRelease:
yaml# clusters/dev/apps/my-service/helmrelease.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: my-service
namespace: dev
spec:
chart:
spec:
chart: ./charts/my-service
sourceRef:
kind: GitRepository
name: flux-system
values:
replicaCount: 1
ingress:
host: my-service.dev.example.com
env:
LOG_LEVEL: "debug"
The chart's values.yaml provides all defaults; the HelmRelease overrides only env-specific keys. All environments share the same chart; only the HelmRelease values files differ.
Validating the merge result
bash# Render merged values without deploying
helm template my-service ./charts/my-service \
-f charts/my-service/values.yaml \
-f charts/my-service/values-dev.yaml \
| grep -A5 "resources:"
# Show the effective values after merging
helm upgrade --install my-service ./charts/my-service \
-f charts/my-service/values.yaml \
-f charts/my-service/values-dev.yaml \
--dry-run \
| grep -A20 "COMPUTED VALUES:"
Use this in CI to verify the merged manifest before helm upgrade.
What not to put in values-env.yaml
- Secrets (passwords, tokens) — use SealedSecrets or ESO
- Image tags — pin in the HelmRelease, managed by Flux image automation
- Topology spread constraints — usually the same across envs
The --set trap
bash# DON'T DO THIS in CI
helm upgrade my-service ./charts/my-service \
--set image.tag="${CI_COMMIT_SHORT_SHA}" \
--set replicaCount=2
--set overrides are not in version control. If someone runs helm rollback, the --set values are gone. All config should live in values files committed to Git.
The one legitimate use of --set in CI: the image tag, if you're not using Flux image automation.
Secrets injection via Helm values
For secrets, don't put them in values files — reference them from a mounted Secret:
yaml# values.yaml
env: {} # populated below
envFromSecret:
- secretRef:
name: my-service-secret
# In the deployment template
envFrom:
{{- range .Values.envFromSecret }}
- secretRef:
name: {{ .secretRef.name }}
{{- end }}
The Secret is created separately by ESO or SealedSecrets. The chart references it by name. No secrets in Git.
Environment promotion workflow
1. Deploy to dev: helm upgrade --install ... -f values-dev.yaml
2. Test in dev
3. Update image tag in values-dev.yaml (or via Flux image automation)
4. When ready: cherry-pick tag change to values-prod.yaml and apply
With FluxCD, this becomes: update image tag in the prod HelmRelease, commit, push — Flux reconciles automatically.