Helmfile: Managing Multiple Helm Releases
Quick answer
Helmfile declaratively manages multiple Helm releases as a unit — one file, one command, consistent across environments. Covers repositories, releases, environments, value layering, and CI integration.
intermediate · 45 min
Before you begin
- Helm Fundamentals — charts, releases, values
- kubectl configured against a cluster
Helmfile: Managing Multiple Helm Releases
Helm manages individual releases. When you have five services and three environments, you're running fifteen helm upgrade --install commands with different value files. Tracking which version is deployed where, and in what order to apply them, becomes manual work.
Helmfile declares all releases in a single helmfile.yaml — repositories, charts, versions, values per environment. One command syncs everything.
Installing Helmfile
1# macOS
2brew install helmfile
3
4# Linux (download binary)
5HELMFILE_VERSION=0.168.0
6curl -Lo helmfile.tar.gz \
7 "https://github.com/helmfile/helmfile/releases/download/v${HELMFILE_VERSION}/helmfile_${HELMFILE_VERSION}_linux_amd64.tar.gz"
8tar -xzf helmfile.tar.gz helmfile
9chmod +x helmfile
10sudo mv helmfile /usr/local/bin/
11
12# Verify
13helmfile --versionHelmfile also requires the helm-diff plugin:
helm plugin install https://github.com/databus23/helm-diffhelm-diff is used by helmfile diff and helmfile apply to show what would change before applying.
helmfile.yaml Structure
1# helmfile.yaml
2
3repositories:
4 - name: bitnami
5 url: https://charts.bitnami.com/bitnami
6 - name: ingress-nginx
7 url: https://kubernetes.github.io/ingress-nginx
8 - name: cert-manager
9 url: https://charts.jetstack.io
10
11releases:
12 - name: ingress-nginx
13 namespace: ingress-nginx
14 createNamespace: true
15 chart: ingress-nginx/ingress-nginx
16 version: "4.10.0"
17 values:
18 - values/ingress-nginx.yaml
19
20 - name: cert-manager
21 namespace: cert-manager
22 createNamespace: true
23 chart: cert-manager/cert-manager
24 version: "1.14.0"
25 set:
26 - name: installCRDs
27 value: true
28
29 - name: my-api
30 namespace: production
31 createNamespace: true
32 chart: ./charts/my-api # Local chart
33 version: "1.2.0"
34 values:
35 - values/common.yaml
36 - values/production.yamlEach release maps to a helm install/upgrade call. All the same options available.
Core Commands
1# Show what would change (no apply)
2helmfile diff
3
4# Sync all releases (install or upgrade to match helmfile.yaml)
5helmfile sync
6
7# Apply only what has changed (diff + sync, skips unchanged releases)
8helmfile apply
9
10# Destroy all releases
11helmfile destroy
12
13# Render all templates without connecting to the cluster
14helmfile template
15
16# List releases and their status
17helmfile list
18
19# Lint all charts
20helmfile lint
21
22# Download chart dependencies
23helmfile depsapply vs sync
helmfile sync — always runs helm upgrade --install for every release, even if nothing changed.
helmfile apply — runs helm diff first. Only upgrades releases where the diff shows actual changes. Use this in CI/CD to avoid unnecessary rollouts.
Targeting Specific Releases
1# Only operate on releases matching a label
2helmfile --selector name=my-api apply
3
4# Multiple selectors (AND logic)
5helmfile --selector namespace=production,chart=my-api apply
6
7# Operate on a single release by name
8helmfile --selector name=cert-manager syncAdd labels to releases for easier targeting:
releases:
- name: my-api
labels:
app: my-api
tier: backend
env: productionEnvironments
Environments let you use the same helmfile.yaml with different values per environment.
1# helmfile.yaml
2
3environments:
4 default:
5 values:
6 - environments/default.yaml
7 staging:
8 values:
9 - environments/staging.yaml
10 production:
11 values:
12 - environments/production.yaml
13 secrets:
14 - environments/production-secrets.yaml # Encrypted with helm-secrets
15
16releases:
17 - name: my-api
18 namespace: "{{ .Environment.Name }}"
19 chart: ./charts/my-api
20 values:
21 - values/common.yaml
22 - values/{{ .Environment.Name }}.yaml1# environments/staging.yaml
2replicaCount: 1
3image:
4 tag: "latest"
5resources:
6 requests:
7 cpu: 100m
8 memory: 128Mi1# environments/production.yaml
2replicaCount: 3
3image:
4 tag: "1.2.0"
5resources:
6 requests:
7 cpu: 500m
8 memory: 512MiApply for a specific environment:
helmfile --environment staging apply
helmfile --environment production applyUsing environment values in helmfile.yaml
1releases:
2 - name: my-api
3 namespace: "{{ .Environment.Name }}"
4 chart: ./charts/my-api
5 values:
6 - values/common.yaml
7 - values/{{ .Environment.Name }}.yaml
8 set:
9 - name: image.tag
10 value: "{{ .Values.image.tag }}" # From environment values fileValue Layering
Values files are merged in order — later files win on conflicts:
1releases:
2 - name: my-api
3 chart: ./charts/my-api
4 values:
5 - values/defaults.yaml # Base defaults
6 - values/{{ .Environment.Name }}.yaml # Environment overrides
7 - values/secrets.yaml # Secret overrides (last wins)You can also use set for individual overrides:
set:
- name: image.tag
value: "{{ requiredEnv \"APP_VERSION\" }}" # Read from env var, fail if unset
- name: replicaCount
value: 3requiredEnv fails if the environment variable isn't set. env returns an empty string instead.
Release Ordering with needs
Some releases must be installed before others (e.g., CRDs before controllers, databases before apps):
1releases:
2 - name: cert-manager
3 namespace: cert-manager
4 chart: cert-manager/cert-manager
5 version: "1.14.0"
6 set:
7 - name: installCRDs
8 value: true
9
10 - name: cluster-issuer
11 namespace: cert-manager
12 chart: ./charts/cluster-issuer
13 needs:
14 - cert-manager/cert-manager # Wait for cert-manager before installing
15
16 - name: my-api
17 namespace: production
18 chart: ./charts/my-api
19 needs:
20 - cert-manager/cluster-issuer
21 - production/my-db # Also wait for DBThe format for needs is <namespace>/<release-name>.
Hooks
Run commands before or after helmfile operations:
1releases:
2 - name: my-api
3 chart: ./charts/my-api
4 hooks:
5 - events: ["presync"]
6 command: "kubectl"
7 args:
8 - "apply"
9 - "-f"
10 - "crds/"
11 - events: ["postsync"]
12 command: "kubectl"
13 args:
14 - "rollout"
15 - "status"
16 - "deployment/my-api"
17 - "-n"
18 - "production"Available events: presync, postsync, preuninstall, postuninstall.
Secrets with helm-secrets
helm-secrets integrates SOPS (age/AWS KMS/GCP KMS) for secret encryption:
1helm plugin install https://github.com/jkroepke/helm-secrets
2
3# Create an age key pair
4age-keygen -o key.txt
5
6# Encrypt a values file
7helm secrets encrypt values/production-secrets.yaml > values/production-secrets.yaml.encReference encrypted files in helmfile:
1releases:
2 - name: my-api
3 chart: ./charts/my-api
4 values:
5 - values/common.yaml
6 secrets:
7 - values/production-secrets.yaml # helm-secrets decrypts at runtimeSOPS_AGE_KEY_FILE=key.txt helmfile applyCI/CD Integration
GitHub Actions
1# .github/workflows/deploy.yml
2name: Deploy
3on:
4 push:
5 branches: [main]
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12
13 - name: Install Helm
14 uses: azure/setup-helm@v3
15 with:
16 version: v3.14.0
17
18 - name: Install Helmfile
19 run: |
20 curl -Lo helmfile.tar.gz \
21 https://github.com/helmfile/helmfile/releases/download/v0.168.0/helmfile_0.168.0_linux_amd64.tar.gz
22 tar -xzf helmfile.tar.gz helmfile
23 sudo mv helmfile /usr/local/bin/
24 helm plugin install https://github.com/databus23/helm-diff
25
26 - name: Configure kubeconfig
27 run: |
28 mkdir -p ~/.kube
29 echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
30
31 - name: Deploy
32 run: helmfile --environment production apply
33 env:
34 APP_VERSION: ${{ github.sha }}Difference preview on PRs
- name: Helmfile diff
run: helmfile --environment staging diff
continue-on-error: trueAdd continue-on-error: true so the diff step doesn't fail the job — it's informational output for the PR review.
Directory Layout for Large Projects
infrastructure/
├── helmfile.yaml
├── environments/
│ ├── default.yaml
│ ├── staging.yaml
│ └── production.yaml
├── values/
│ ├── ingress-nginx.yaml
│ ├── cert-manager.yaml
│ ├── my-api-common.yaml
│ ├── my-api-staging.yaml
│ └── my-api-production.yaml
└── charts/
└── my-api/ # Local chart
For very large setups, split into multiple helmfiles:
# helmfile.yaml
helmfiles:
- path: infrastructure/helmfile.yaml # ingress, cert-manager
- path: apps/helmfile.yaml # application releases
selectors:
- "env={{ .Environment.Name }}"Frequently Asked Questions
What does Helmfile add over plain Helm?
Declarative management of many releases at once, with environments, value layering and ordering between them. Instead of a script running several helm upgrade commands in sequence, you describe the desired set of releases and apply it. The value grows with the number of releases you manage together.
How does value layering resolve?
Later sources win, so environment-specific files override defaults and inline values override files. The common mistake is expecting a deep merge where a list is concatenated — lists replace rather than merge, so a partially specified list silently drops the rest.
Should I use Helmfile or a GitOps controller?
They solve different halves. Helmfile is a client-side tool you run, so someone or something must run it. A GitOps controller reconciles continuously and detects drift. Helmfile suits teams comfortable driving deploys from CI; a controller suits teams who want the cluster to converge on Git without anyone running a command.
How do I preview changes before applying?
Use the diff command, which shows what would change across every release rather than per chart. That whole-set view is the main practical advantage over running Helm by hand — you see the blast radius before committing to it.
What's Next
- Helm Fundamentals — charts, releases, and value overrides
- Kubernetes Core Concepts — the Deployments and Services that Helmfile manages
- ArgoCD Setup and Automated Sync — GitOps alternative to imperative Helmfile runs
Official References
- Helm chart template guide — templates, values and the sprig function set
- Helm charts — chart structure, dependencies and hooks
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.