Part ofHelm: Kubernetes Package Management·Step 2 of 2
Kubernetes

Helmfile: Managing Multiple Helm Releases

Intermediate45 min to complete14 min readJune 1, 2026Updated August 19, 2026

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
Helm
Kubernetes
GitOps
DevOps
IaC

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

bash
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 --version

Helmfile also requires the helm-diff plugin:

bash
helm plugin install https://github.com/databus23/helm-diff

helm-diff is used by helmfile diff and helmfile apply to show what would change before applying.


helmfile.yaml Structure

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

Each release maps to a helm install/upgrade call. All the same options available.


Core Commands

bash
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 deps

apply 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

bash
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 sync

Add labels to releases for easier targeting:

yaml
releases:
  - name: my-api
    labels:
      app: my-api
      tier: backend
      env: production

Environments

Environments let you use the same helmfile.yaml with different values per environment.

yaml
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 }}.yaml
yaml
1# environments/staging.yaml
2replicaCount: 1
3image:
4  tag: "latest"
5resources:
6  requests:
7    cpu: 100m
8    memory: 128Mi
yaml
1# environments/production.yaml
2replicaCount: 3
3image:
4  tag: "1.2.0"
5resources:
6  requests:
7    cpu: 500m
8    memory: 512Mi

Apply for a specific environment:

bash
helmfile --environment staging apply
helmfile --environment production apply

Using environment values in helmfile.yaml

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 file

Value Layering

Values files are merged in order — later files win on conflicts:

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

yaml
set:
  - name: image.tag
    value: "{{ requiredEnv \"APP_VERSION\" }}"    # Read from env var, fail if unset
  - name: replicaCount
    value: 3

requiredEnv 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):

yaml
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 DB

The format for needs is <namespace>/<release-name>.


Hooks

Run commands before or after helmfile operations:

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

bash
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.enc

Reference encrypted files in helmfile:

yaml
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 runtime
bash
SOPS_AGE_KEY_FILE=key.txt helmfile apply

CI/CD Integration

GitHub Actions

yaml
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

yaml
- name: Helmfile diff
  run: helmfile --environment staging diff
  continue-on-error: true

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

yaml
# 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

Official References

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.