Learn Helm Chart - Multi-Environment Management
Episode 21 of 30

Learn Helm Chart - Multi-Environment Management

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.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

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.

Main Discussion

Environment Strategy: One Chart, Many Values

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.

Values File Organization: values-common, values-dev, values-staging, values-prod

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:

Values directory 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.yaml

Why 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:

deploy/values-common.yaml
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-secret

And here's the values-prod.yaml example — the file most dissected during production audits:

deploy/values-prod.yaml
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: info

Notice 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.

Deployment Workflow: From Dev to Production via a Promotion Pipeline

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:

Deploying to production
helm upgrade --install myapp ./charts/myapp \
  -f deploy/values-common.yaml \
  -f deploy/values-prod.yaml \
  -n prod \
  --atomic \
  --timeout 10m

The 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:

  1. Dev: every push to the develop branch triggers an automatic deploy to the dev namespace — fast feedback, allowed to break.
  2. Staging: every merge to main triggers a deploy to staging — here the application is tested exactly like production, including database migrations and smoke tests.
  3. Production: the deploy to 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.

Environment Parity: As Close As Possible, With Only Deliberate Differences

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:

AspectDevStagingProd
Replica count136
Resource request/limitSmallMediumLarge
DatabaseEphemeral (SQLite/minio)Managed, dummy dataManaged, real data
Feature flagsAll activeSimilar to prodPer strategy
TLS / ingressOptionalRequiredRequired
SecretsDummySimilar to prod, not realReal 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.

Helmfile: Declaring All Releases

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:

helmfile.yaml - full example
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-manager

Let'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 operations
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 releases

helmfile 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.

Conclusion

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:

  • One chart, many values — never fork charts per environment.
  • Identical deploy commands — only the values file and namespace differ.
  • Promotion, not re-deploy — the artifact tested in staging is what goes to production.
  • Deliberate differences — document resource sizing, replicas, and feature flags per environment.
  • Helmfile declares the systemneeds: 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!