How do you manage environment-specific configuration with Helm?
Quick Answer
Maintain a base `values.yaml` with sensible defaults, and create separate, smaller values files per environment (`values-dev.yaml`, `values-production.yaml`) containing only the values that actually differ — passed at install/upgrade time with `--values` (which can be supplied multiple times, later files overriding earlier ones), or individual overrides via `--set` for quick one-off changes. This keeps environment differences explicit and minimal, without duplicating the entire configuration or the chart's templates per environment.
Detailed Answer
Layering values files
# values.yaml -- the chart's baseline defaults
replicaCount: 2
image:
repository: myapp
tag: "1.0.0"
resources:
requests:
cpu: "100m"
memory: "128Mi"
autoscaling:
enabled: false
# values-production.yaml -- ONLY the values that differ from the defaults
replicaCount: 6
resources:
requests:
cpu: "500m"
memory: "512Mi"
autoscaling:
enabled: true
minReplicas: 6
maxReplicas: 20
helm install my-app ./mychart -f values.yaml -f values-production.yaml
Helm merges values files in the order given, with later files overriding earlier ones for any key they both specify — so values-production.yaml only needs to contain the handful of keys that genuinely differ for production, not a full duplicate copy of every setting.
Quick one-off overrides with --set
helm upgrade my-app ./mychart -f values-production.yaml --set image.tag=2.1.0
--set is convenient for a single, temporary override (like bumping just the image tag as part of a CI/CD pipeline step) without needing to edit or generate a values file — but it's harder to track/audit than a values file checked into version control, so it's generally better suited to programmatic use (a deploy script substituting in a build-specific value) than for hand-maintained, meaningful configuration differences.
A common project layout
mychart/
├── values.yaml # defaults
environments/
├── values-dev.yaml
├── values-staging.yaml
└── values-production.yaml
Keeping environment-specific values files outside the chart itself (rather than bundled inside it) is a common pattern, since it cleanly separates "what the chart is" (reusable, environment-agnostic templates and sensible defaults) from "how a specific environment configures it" (which changes far more often and is often owned/reviewed by a different part of the team, like a platform/SRE group rather than the application developers who own the chart's templates).
Secrets shouldn't live in plain values files
Values files are ordinary, unencrypted text — genuinely sensitive values (database passwords, API keys) shouldn't be committed directly in a values-production.yaml any more than they should in a raw Kubernetes Secret manifest (see the security topic's Secrets question). Common approaches: reference an already-existing Kubernetes Secret (created out-of-band, via an external secrets manager) from the chart's templates rather than passing the actual secret value through values.yaml at all, or use a values-file encryption tool (like helm-secrets, built on SOPS) so sensitive values files themselves can be safely committed in encrypted form.
Validating what will actually be applied, before applying it
helm template my-app ./mychart -f values-production.yaml
helm template renders the final Kubernetes manifests locally without installing anything — an essential sanity check before running a real helm upgrade against production, letting you review the exact resulting YAML (confirming the right image tag, replica count, resource limits actually got substituted correctly) rather than trusting the values-merging logic blindly.
Keep the base values.yaml as the single source of sensible, safe defaults; keep per-environment files minimal (only the actual deltas); use --set sparingly and mainly for CI/CD-driven, single-value substitutions; and never commit real secret values into any values file, encrypted or not, without a specific reason.