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. Pass them at install/upgrade time with --values, which can be supplied multiple times with later files overriding earlier ones, or use 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. 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.