Skip to content

Helm vs Kustomize

Use Helm when you have things that are identical except for one or two config changes, and you want many copies. Other than that, treat it like a package manager.

People use Helm to create a template for Kubernetes resources. If they have configured Kubernetes to work with e.g. mkcert, superset etc., use their chart and call it a day.

If you have things that have a lot of overlap (e.g. same front/back/db split e.g. Rach and Stone & Chalk), then use an override on a base, hoist duplication up where possible.

If there’s limited overlap (e.g. Uni Remit and National Intermodal), use separate charts/bases entirely — never force shared structure as it adds complexity. See Helm Charts in Kustomize for details on the rough edges of mixing the two.

graph TD
    A["New Kubernetes resource"] --> B{"Who wrote it?"}

    B -->|"Someone else<br />(open source, vendor)"| C["✅ Helm<br />install their chart<br />override with values.yaml"]

    B -->|"You wrote it"| D{"Will other teams install it?"}

    D -->|"Yes"| E["✅ Helm<br />package as a chart<br />publish to a repo"]

    D -->|"No, only you deploy it"| F{"What kind of resource?"}

    F -->|"App workload<br />(deployment, service, ingress)"| G{"Are your apps structurally identical?"}
    F -->|"Cluster policy<br />(RBAC, NetworkPolicy,<br />namespaces, quotas)"| H["✅ Kustomize<br />plain YAML, auditable<br />overlays per cluster"]

    G -->|"Yes, same app<br />different config"| I["✅ Helm<br />one chart, many values files"]
    G -->|"Mostly similar<br />some structural differences"| J["✅ Kustomize<br />base + per-site overlays"]
    G -->|"Completely different"| K["Separate charts<br />or separate bases"]
graph LR
    subgraph "Kustomize (you own it, plain YAML)"
        direction TB
        K1["infrastructure/base/"]
        K1 --> K2["namespaces"]
        K1 --> K3["RBAC roles + bindings"]
        K1 --> K4["NetworkPolicies"]
        K1 --> K5["ResourceQuotas"]

        K6["infrastructure/overlays/"]
        K6 --> K7["doks/<br />24 namespaces"]
        K6 --> K8["k0s/<br />12 namespaces"]
    end

    subgraph "Helm (someone else wrote it)"
        direction TB
        H1["Third-party charts"]
        H1 --> H2["ingress-nginx"]
        H1 --> H3["cert-manager"]
        H1 --> H4["postgres-operator"]
    end

    subgraph "Helm (you wrote it, same app × 12)"
        direction TB
        H5["charts/website/"]
        H5 --> H6["templates/<br />deployment, service,<br />ingress, serviceaccount"]
        H5 --> H7["values/<br />website-a.yaml<br />website-b.yaml<br />...website-l.yaml"]
    end

    K7 -->|"kubectl apply -k"| CLUSTER["☸ DOKS Cluster"]
    K8 -->|"kubectl apply -k"| LOCAL["☸ k0s Cluster"]
    H1 -->|"helm install"| CLUSTER
    H5 -->|"helm install × 12"| CLUSTER
    H5 -->|"helm install × 12"| LOCAL
graph TD
    A["12 websites to deploy"] --> B{"How similar are they?"}

    B -->|"Identical app<br />just different config<br />(domain, DB, env vars)"| C["One Helm chart<br />12 values files"]
    C --> C1["charts/website/"]
    C --> C2["values/website-a.yaml<br />values/website-b.yaml<br />..."]
    C --> C3["helm install × 12"]

    B -->|"Mostly the same<br />but some have extras<br />(sidecars, cron jobs)"| D["Kustomize<br />base + overlays"]
    D --> D1["apps/base/<br />common deployment"]
    D --> D2["apps/overlays/website-a/<br />apps/overlays/website-c/<br />(patches for differences)"]
    D --> D3["kubectl apply -k × 12"]

    B -->|"Different apps<br />that happen to be websites"| E["Separate charts<br />or separate bases"]
    E --> E1["Don't force<br />shared structure"]