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.