Managing one chart across many environments: values file organization strategy (common, dev, staging, prod), deployment workflow and promotion pipelines, environment parity, and Helmfile as a comprehensive release declaration with environment management and inter-release dependencies.

After episode 20, where we covered chart security — from security context and RBAC to signing and admission controllers — in this episode we cover a problem that equally often wrecks DevOps teams: how to manage the same chart for different environments. The moment your team grows from "we have one staging cluster" to "we have dev, staging, prod, even per-branch" is the moment everything can fall apart if not set up with the right strategy.
Think of the chart as a recipe, and environments as the kitchens where the recipe is cooked. The recipe is the same — the base ingredients are identical — but in the dev kitchen you cook for one or two people over medium heat, in the staging kitchen you test the recipe exactly like production, and in the production kitchen you cook for thousands of people under the strictest hygiene standards. You don't rewrite the recipe for each kitchen; you adjust the portions, the stove temperature, and the ingredient standards.
The problems that arise without a good strategy can be seen in many teams: a values.yaml full of # TODO: change for prod comments, values overwritten manually via --set recorded nowhere, or — worst of all — three copies of a chart forked for three environments that slowly diverge until no one knows which is correct.
In this episode we dissect: environment strategy in general, proper values file organization, deployment workflow and promotion pipelines, environment parity, and close with Helmfile — a tool that turns a series of ad-hoc Helm commands into a complete, reviewable, auditable release declaration.
Three approaches circulate in the industry, each with trade-offs:
1. One chart + many values files. One fixed chart, each environment has its own values file stacked on top of the default values. This is the most common approach and the most recommended for small-to-medium teams. Advantages: one chart code source, and diffs between environments are easy to see by comparing values files. Weakness: if environments need large structural differences (not just values), the chart can be forced to accommodate many conditionals that get hard to maintain.
2. Environment-specific charts. A separate chart per environment — for example, myapp-dev and myapp-prod. This is an anti-pattern for most cases: application logic is duplicated, bug fixes have to be copied to three places, and drift becomes inevitable. The only case justifying this is when environments genuinely run structurally different application versions — and even then, it's better solved with library charts or hooks.
3. Overlay patterns. Mirroring the Kustomize model: one base values, then per-environment overlays that override parts. In Helm, this is realized through the values file merge mechanism we learned in episode 6 — values files don't replace each other wholesale, but are merged with values from the higher file overriding the lower.
The Helm merge hierarchy you must memorize (lowest to highest priority): the chart's built-in values.yaml → the first -f file → the second -f file → ... → inline --set. This order is the key to every strategy we'll discuss.
The pattern most widely used in industry is splitting values files into two layers: common (values that are the same in every environment) and per-environment (values that differ). The ideal repository structure:
myapp/
├── charts/ # main chart
│ └── myapp/
│ ├── Chart.yaml
│ ├── values.yaml # built-in chart defaults
│ └── templates/
└── deploy/
├── values-common.yaml # same for all environments
├── values-dev.yaml
├── values-staging.yaml
└── values-prod.yamlWhy is values-common.yaml important when the chart's built-in values.yaml already exists? Because there are values that must be explicitly reviewed in the deployment repo — for example, an agreed-upon image version, or organization-specific configuration that's the same across all environments. Separating them makes PR review more focused: a change in values-common.yaml means "a change affecting all environments" — something that must be approved more strictly than a change in values-dev.yaml.
A realistic values-common.yaml example:
replicaCount: 2
image:
repository: ghcr.io/myorg/myapp
tag: 1.24.0
pullPolicy: Always
serviceAccount:
create: true
name: myapp
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
imagePullSecrets:
- name: ghcr-pull-secretAnd here's the values-prod.yaml example — the file most dissected during production audits:
replicaCount: 6
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
autoscaling:
enabled: true
minReplicas: 4
maxReplicas: 12
targetCPUUtilizationPercentage: 70
ingress:
enabled: true
className: nginx
hosts:
- host: app.mycompany.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- app.mycompany.com
secretName: app-tls
featureFlags:
paymentsEnabled: true
betaFeatures: false
env:
NODE_ENV: production
LOG_LEVEL: infoNotice what's not in this file: no secrets, no usernames, no tokens. All sensitive values come from the External Secrets Operator (the episode 20 pattern). This file only contains configuration — so it's safe to store in Git and review casually.
With the values file structure above, the deploy flow per environment becomes a nearly identical series of commands — only the values file and namespace differ:
helm upgrade --install myapp ./charts/myapp \
-f deploy/values-common.yaml \
-f deploy/values-prod.yaml \
-n prod \
--atomic \
--timeout 10mThe strength of this pattern: the same command is used in every environment, so there's no "different deployment style." The only difference is the arguments. And because the entire command is recorded in a pipeline (not typed manually on a laptop), every deploy is auditable: when, from which version, to which environment.
A promotion pipeline is the concept that makes this workflow disciplined: nothing deploys to production without going through staging first. The flow is roughly:
develop branch triggers an automatic deploy to the dev namespace — fast feedback, allowed to break.main triggers a deploy to staging — here the application is tested exactly like production, including database migrations and smoke tests.prod is not automatic — it needs a manual trigger (or approval) and only from an artifact already proven in staging.The key to healthy promotion: the same artifact tested in staging must be the one deployed to production. That's why the image version and chart version are pinned in values-common.yaml, and the staging result must be "signed off" (via CI test status) before promotion. You're not deploying different values — you're deploying the same thing to a more risky place.
The multi-environment paradox is: you want environments as close to production as possible (so "works in dev" truly means "works in prod"), but you also don't want environments to be as expensive and strict as production. The answer isn't "everything must be the same" but rather differences must be deliberate and documented, not the result of uncontrolled drift.
Reasonable differences between environments:
| Aspect | Dev | Staging | Prod |
|---|---|---|---|
| Replica count | 1 | 3 | 6 |
| Resource request/limit | Small | Medium | Large |
| Database | Ephemeral (SQLite/minio) | Managed, dummy data | Managed, real data |
| Feature flags | All active | Similar to prod | Per strategy |
| TLS / ingress | Optional | Required | Required |
| Secrets | Dummy | Similar to prod, not real | Real from vault |
Differences that are not reasonable: different image versions between staging and prod, configuration that should be the same but was set manually, or charts no longer in sync with the repo. These cause the classic "passed in staging, broke in production" phenomenon.
Techniques that keep parity close: use --set as little as possible (because it's not recorded in Git — a values file is better), make values-prod.yaml the reference and ensure staging is rendered from the same structure, and automate a "promotion check" verifying that the chart and values used in staging are identical to what will be used in prod.
Tip
Use helm template to diff between environments. Before deploying, render the chart with -f values-staging.yaml and -f values-prod.yaml, then compare the output: helm template myapp ./charts/myapp -f deploy/values-common.yaml -f deploy/values-staging.yaml > /tmp/staging.yaml and helm template ... -f deploy/values-prod.yaml > /tmp/prod.yaml, then diff /tmp/staging.yaml /tmp/prod.yaml. This explicitly shows every manifest difference that will be used — the fastest way to catch configuration that accidentally differs.
After a few charts — say myapp, redis, ingress-nginx, and cert-manager — the workflow of running helm upgrade --install one by one starts feeling fragile: a wrong order can cause cascading failures, different flags must be remembered, and there's no single place describing "the whole system." That's where Helmfile comes in.
Helmfile is a tool that declares all Helm releases in one YAML file, complete with environments, values, and inter-release dependencies. It's "Terraform for Helm" — and it's popular in platform engineering precisely because one helmfile.yaml file is executable documentation of what runs in the cluster.
The core structure of a helmfile.yaml:
environments:
prod:
values:
- deploy/values-common.yaml
- deploy/values-prod.yaml
staging:
values:
- deploy/values-common.yaml
- deploy/values-staging.yaml
dev:
values:
- deploy/values-common.yaml
- deploy/values-dev.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
- name: ingress-nginx
url: https://kubernetes.github.io/ingress-nginx
releases:
- name: ingress-nginx
namespace: ingress-nginx
chart: ingress-nginx/ingress-nginx
version: 4.9.0
createNamespace: true
- name: cert-manager
namespace: cert-manager
chart: jetstack/cert-manager
version: 1.14.4
createNamespace: true
needs:
- ingress-nginx/ingress-nginx
- name: myapp
namespace: myapp
chart: ./charts/myapp
values:
- deploy/values-common.yaml
valuesTemplate:
- "{{ .Values | toYaml }}"
needs:
- cert-manager/cert-managerLet's dissect the important parts:
environments defines per-environment values referenceable within the file. When running helmfile -e prod apply, the {{ .Values }} variable will contain the combination of values-common.yaml + values-prod.yaml. This lets one helmfile serve all environments without duplication.
releases declares each release. Notice the needs: — this is the inter-release dependency that makes Helmfile run releases in the correct order: cert-manager waits for ingress-nginx to be ready (because it needs the IngressClass), and myapp waits for cert-manager (because its TLS is managed by it). Without needs, install order isn't guaranteed — and that's the classic cause of "cert-manager failed because the IngressClass doesn't exist yet."
valuesTemplate is a feature that makes Helmfile very powerful: it can render values from the helmfile's own environment variables, so the entire environment configuration flows into the chart without error-prone manual -f commands.
How to use it:
helmfile -e dev apply # apply all releases in dev
helmfile -e staging diff # view changes before applying (dry-run)
helmfile -e prod apply --confirm # apply with manual confirmation
helmfile -e prod destroy # delete all releases
helmfile -e dev list # view the status of all releaseshelmfile apply is the heart — it diffs, asks for confirmation, then applies the changed releases. This is state management in its most practical form: Helmfile stores state in the cluster (via Helm release status), so it knows which releases are installed and what changed — not just "reinstall everything" like a naive script.
Note
Helmfile vs ArgoCD/Flux. The two have different roles. Helmfile is an orchestrator run in a pipeline — it executes when you run it (or via CI). ArgoCD and Flux are GitOps controllers running continuously in the cluster — they watch Git and sync state automatically. Helmfile suits push-based workflows (pipelines), GitOps tools suit pull-based workflows (which we'll cover in detail in episode 25). Many production teams actually use both: Helmfile to manage bootstrap and complex releases, a GitOps tool for day-to-day drift detection.
In this episode 21 we covered how to manage one chart across many environments with discipline: environment strategy (one chart + values files, not chart forks), values file organization (values-common for what's uniform, values-dev/staging/prod for what differs), deployment workflow with a promotion pipeline flowing from dev to staging to production, environment parity ensuring differences between environments are decisions, not drift, and Helmfile, which declares all releases along with their dependencies and environment values in one executable file.
The core takeaways:
needs: for ordering, environments: for values, apply for state management.In the next episode, episode 22, we cover Helm plugins: the ecosystem extending Helm beyond its built-in capabilities — from helm-diff visualizing changes before they're applied, helm-secrets for encryption, helm-unittest for testing, to how to create your own plugins. See you in episode 22!