Helmfile: Manage Multiple Helm Releases Like a Pro
Once you're managing more than a handful of Helm releases across multiple environments, running individual helm upgrade commands starts to fall apart. You end up with shell scripts full of flags, inconsistent values between environments, and no reliable way to see what's actually deployed. Helmfile solves this by giving you a declarative, file-based way to manage your entire Helm release state — and it fits naturally into a GitOps workflow.
What Helmfile Actually Is
Helmfile is a wrapper around Helm. It reads a helmfile.yaml (or a directory of them) and translates your desired state into helm install, helm upgrade, and helm uninstall commands. The key difference from raw Helm: everything is declared in files, not in shell history.
# install
brew install helmfile
# or with asdf
asdf plugin add helmfile
asdf install helmfile 0.163.1
asdf global helmfile 0.163.1
# verify
helmfile version
A Real-World helmfile.yaml
Here's what a production helmfile looks like for a typical platform:
# helmfile.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
- name: ingress-nginx
url: https://kubernetes.github.io/ingress-nginx
- name: cert-manager
url: https://charts.jetstack.io
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
environments:
staging:
values:
- environments/staging.yaml
production:
values:
- environments/production.yaml
releases:
- name: ingress-nginx
namespace: ingress-nginx
chart: ingress-nginx/ingress-nginx
version: 4.10.1
values:
- charts/ingress-nginx/values.yaml
- charts/ingress-nginx/values-{{ .Environment.Name }}.yaml
- name: cert-manager
namespace: cert-manager
chart: cert-manager/cert-manager
version: v1.14.4
values:
- charts/cert-manager/values.yaml
set:
- name: installCRDs
value: true
- name: postgresql
namespace: data
chart: bitnami/postgresql
version: 15.5.17
values:
- charts/postgresql/values.yaml
- charts/postgresql/values-{{ .Environment.Name }}.yaml
secrets:
- charts/postgresql/secrets-{{ .Environment.Name }}.yaml
- name: myapp
namespace: default
chart: ./charts/myapp
version: ~> 1.4
needs:
- data/postgresql
- ingress-nginx/ingress-nginx
values:
- charts/myapp/values.yaml
- charts/myapp/values-{{ .Environment.Name }}.yaml
The {{ .Environment.Name }} template variable is one of the most useful Helmfile features — it lets a single release definition load different values files per environment. No duplication.
Directory Structure
For anything beyond a toy setup, organize your Helmfile config like this:
helmfile/
├── helmfile.yaml # root file, imports all
├── environments/
│ ├── staging.yaml
│ └── production.yaml
├── charts/
│ ├── ingress-nginx/
│ │ ├── values.yaml
│ │ ├── values-staging.yaml
│ │ └── values-production.yaml
│ ├── cert-manager/
│ │ └── values.yaml
│ ├── postgresql/
│ │ ├── values.yaml
│ │ ├── values-staging.yaml
│ │ ├── values-production.yaml
│ │ ├── secrets-staging.yaml # sops-encrypted
│ │ └── secrets-production.yaml # sops-encrypted
│ └── myapp/
│ ├── values.yaml
│ ├── values-staging.yaml
│ └── values-production.yaml
For larger teams, you can split by team or domain using Helmfile's helmfiles include directive:
# helmfile.yaml (root)
helmfiles:
- path: teams/platform/helmfile.yaml
- path: teams/backend/helmfile.yaml
- path: teams/frontend/helmfile.yaml
Each sub-helmfile owns its own releases, and you can still run them all together from the root.
Environment Values and Layering
The environments/staging.yaml file sets variables that apply across all releases in that environment:
# environments/staging.yaml
clusterName: eks-staging-us-east-1
domain: staging.example.com
replicaCount: 1
storageClass: gp3
monitoring:
enabled: true
namespace: monitoring
ingress:
className: nginx
tlsEnabled: true
Then in a chart's values template, reference these:
# charts/myapp/values.yaml (base)
replicaCount: 1
ingress:
enabled: true
className: nginx
# charts/myapp/values-production.yaml (overrides)
replicaCount: 3
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 1Gi
Layering works as: base values → environment values → release-level set overrides. Later layers win.
Secrets Management with SOPS
Helmfile has native support for SOPS-encrypted secrets files. Install the helmfile-secrets plugin and you're good:
helm plugin install https://github.com/jkroepke/helm-secrets
Encrypt your secrets file:
sops --encrypt --pgp YOUR_GPG_KEY_ID \
charts/postgresql/secrets-production.yaml > \
charts/postgresql/secrets-production.yaml
Reference it in your helmfile:
releases:
- name: postgresql
secrets:
- charts/postgresql/secrets-{{ .Environment.Name }}.yaml
Helmfile will decrypt on the fly when running. The decrypted values are never written to disk — they're passed directly to Helm in memory.
Core Commands
# Dry run — see what would change without applying
helmfile diff --environment staging
# Apply everything
helmfile sync --environment production
# Apply only a specific release
helmfile sync --environment production --selector name=myapp
# Apply only releases with a specific label
helmfile sync --environment production --selector tier=frontend
# Destroy a release
helmfile destroy --environment staging --selector name=myapp
# List all release states
helmfile list
# Fetch all charts without installing (useful in CI to pre-pull)
helmfile fetch
The diff command is your best friend before any production change. It shows exactly what Helm would change — both at the Helmfile level (which releases are added/removed) and at the Kubernetes manifest level (which objects change).
Release Dependencies with needs
The needs field is critical for releases that must start in a specific order:
releases:
- name: postgresql
namespace: data
chart: bitnami/postgresql
- name: redis
namespace: data
chart: bitnami/redis
- name: myapp
namespace: default
chart: ./charts/myapp
needs:
- data/postgresql # format: namespace/release-name
- data/redis
Helmfile will wait for postgresql and redis to reach a healthy state before deploying myapp. This is much cleaner than sleep loops in shell scripts.
Labels for Selective Deployment
Labels are how you do partial deploys — useful when one team only wants to deploy their services:
releases:
- name: myapp-api
labels:
app: myapp
component: api
tier: backend
chart: ./charts/myapp-api
- name: myapp-worker
labels:
app: myapp
component: worker
tier: backend
chart: ./charts/myapp-worker
- name: myapp-frontend
labels:
app: myapp
component: frontend
tier: frontend
chart: ./charts/myapp-frontend
# Deploy only backend components
helmfile sync --selector tier=backend
# Deploy only myapp releases
helmfile sync --selector app=myapp
CI/CD Integration
A typical GitHub Actions workflow:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install tools
run: |
curl -L https://github.com/helmfile/helmfile/releases/download/v0.163.1/helmfile_linux_amd64.tar.gz | tar xz
sudo mv helmfile /usr/local/bin/
helm plugin install https://github.com/jkroepke/helm-secrets
- name: Configure kubectl
uses: aws-actions/amazon-eks-update-kubeconfig@v1
with:
cluster-name: eks-production-us-east-1
- name: Import GPG key for SOPS
run: echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --import
- name: Diff (dry run)
run: helmfile diff --environment production
- name: Deploy
run: helmfile sync --environment production
Comparing helmfile apply vs helmfile sync
This trips people up:
| Command | Behavior |
|---|---|
helmfile sync | Installs/upgrades all releases to match desired state |
helmfile apply | Like sync, but runs diff first and only proceeds if there are changes |
helmfile diff | Shows changes without applying — exit code 2 if there are diffs |
In CI, helmfile apply is usually what you want — it's idempotent and skips no-op deployments efficiently.
When to Use Helmfile vs Argo CD
Helmfile and Argo CD serve overlapping but different needs:
- Helmfile is a CLI tool — you run it, it does the thing. Good for teams not ready for a full GitOps operator, or for bootstrapping a cluster before Argo CD itself is running.
- Argo CD is a controller — it continuously reconciles state. Better for large teams who want drift detection and automatic sync.
Many teams use both: Helmfile to bootstrap Argo CD and cluster-level infrastructure, then Argo CD for application releases. The two tools don't conflict.
Helmfile is one of those tools that feels unnecessary until you're managing ten releases across three environments, and then you can't imagine going back to raw Helm commands.
Was this article helpful?
DevSecOps Lead
Security-first mindset in everything I ship. From zero-trust architectures to supply chain security, I make sure your pipeline doesn't become your weakest link.
Related Articles
Helm Chart Best Practices: Structure, Versioning, and Templating
Practical Helm chart best practices covering directory structure, versioning strategies, and templating patterns for production-grade charts.
Helm Hooks and Chart Tests: Lifecycle Management Done Right
Learn how to use Helm hooks for database migrations and pre-deploy tasks, and chart tests to validate releases in Kubernetes.
Helm Error: Release Has No Deployed Revisions - How To Recover Failed Installations
If you've been working with Helm for any length of time, you've probably encountered this frustrating error: "Error: release has no deployed revisions." It...
Helm Error: Repository Name Already Exists - Complete Fix Guide For Duplicate Repo Names
If you've been working with Helm for more than five minutes, you've probably encountered this frustrating error: "repository name already exists". It's one...
Fix Helm 'Another Operation in Progress' Error
Resolve the Helm 'another operation (install/upgrade/rollback) is in progress' error caused by stuck releases with safe recovery steps.
GitOps in Practice: ArgoCD vs Flux for Kubernetes Deployments
Side-by-side comparison of ArgoCD and Flux for GitOps on Kubernetes — architecture differences, sync behavior, multi-cluster, and when to choose each.