Our Pick Helm — Helm's packaging model, huge chart ecosystem, and release lifecycle management make it the right default for most teams. Kustomize wins for teams that want plain YAML overlays without templating complexity, especially in GitOps workflows.
Helm vs Kustomize

import ComparisonTable from ’../../components/ComparisonTable.astro’;

Helm and Kustomize are the two dominant approaches to managing Kubernetes configuration. They take philosophically different approaches — Helm uses templating and packaging; Kustomize uses patch-based overlays on plain YAML.

Quick Verdict

Choose Helm if: You’re deploying complex applications with many configuration options, want to reuse community charts (PostgreSQL, Redis, etc.), or need release management (rollback, history).

Choose Kustomize if: You prefer plain YAML without templating logic, work in a GitOps workflow with ArgoCD/Flux, or are managing configuration variants (dev/staging/prod) of your own apps.


Architecture Comparison

<ComparisonTable headers={[“Dimension”, “Helm”, “Kustomize”]} rows={[ [“Approach”, “Go templates + values”, “Patch-based overlays”], [“Config format”, “Template YAML ({{ }})”, “Plain YAML”], [“Packaging”, “Charts (tar.gz)”, “Directory structure”], [“Distribution”, “Chart repositories”, “Git repos or OCI”], [“Release tracking”, “Built-in (helm list)”, “External (kubectl/ArgoCD)”], [“Rollback”, “helm rollback”, “Git revert”], [“Community ecosystem”, “Huge (ArtifactHub)”, “Limited”], [“Built into kubectl”, “No (separate CLI)”, “Yes (kubectl apply -k)”], [“GitOps friendly”, “Yes (with ArgoCD/Flux)”, “Native fit”], [“Complexity”, “Higher”, “Lower”], [“Secrets management”, “Plugin-based”, “External required”], ]} />


Helm Fundamentals

Helm uses Go templating to generate Kubernetes manifests from a chart:

Chart structure:

my-app/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default values
├── values-prod.yaml    # Production overrides
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── _helpers.tpl    # Template helpers
│   └── NOTES.txt       # Post-install notes
└── charts/             # Chart dependencies

Chart.yaml:

apiVersion: v2
name: my-app
description: My application Helm chart
type: application
version: 1.2.0           # Chart version
appVersion: "2.4.1"      # App version being packaged

values.yaml (defaults):

replicaCount: 1

image:
  repository: ghcr.io/myorg/my-app
  tag: ""                 # Defaults to Chart.appVersion
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  className: nginx
  host: ""
  tls: false

resources:
  limits:
    cpu: 500m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

env:
  DATABASE_URL: ""
  LOG_LEVEL: "info"

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
          env:
            {{- range $key, $val := .Values.env }}
            - name: {{ $key }}
              value: {{ $val | quote }}
            {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Deploying with Helm:

# Install chart
helm install my-release ./my-app \
  --namespace production \
  --create-namespace \
  --set image.tag=v2.4.1 \
  --set ingress.enabled=true \
  --set ingress.host=myapp.example.com \
  -f values-prod.yaml

# List releases
helm list -n production

# Upgrade
helm upgrade my-release ./my-app \
  --namespace production \
  --set image.tag=v2.5.0

# Rollback to previous version
helm rollback my-release 1 -n production

# Show rendered templates (dry run)
helm template my-release ./my-app --debug

# Uninstall
helm uninstall my-release -n production

Using community charts:

# Add chart repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Install PostgreSQL (no chart authoring needed)
helm install my-postgres bitnami/postgresql \
  --set auth.postgresPassword=secretpassword \
  --set primary.persistence.size=20Gi \
  --namespace databases

# Install Redis
helm install my-redis bitnami/redis \
  --set auth.enabled=false \
  --namespace databases

Kustomize Fundamentals

Kustomize works by defining a base configuration and then applying patches per environment:

Directory structure:

k8s/
├── base/
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   └── configmap.yaml
└── overlays/
    ├── staging/
    │   ├── kustomization.yaml
    │   └── replica-patch.yaml
    └── production/
        ├── kustomization.yaml
        ├── replica-patch.yaml
        └── ingress.yaml

base/deployment.yaml (plain YAML, no templating):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: ghcr.io/myorg/my-app:latest
          ports:
            - containerPort: 8080
          resources:
            limits:
              cpu: 200m
              memory: 128Mi

base/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml
  - configmap.yaml

commonLabels:
  app: my-app
  managed-by: kustomize

images:
  - name: ghcr.io/myorg/my-app
    newTag: latest     # Override at overlay level

overlays/production/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
  - ../../base

# Override image tag
images:
  - name: ghcr.io/myorg/my-app
    newTag: "2.4.1"

# Add namespace
namespace: production

# Apply patches
patches:
  - path: replica-patch.yaml
  - path: resource-patch.yaml

# Add resources not in base
resources:
  - ingress.yaml

# Add common labels for production
commonLabels:
  environment: production

overlays/production/replica-patch.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3               # Override base (1 → 3)

Deploying with Kustomize:

# Preview what will be applied
kubectl kustomize overlays/production

# Apply production
kubectl apply -k overlays/production

# Apply staging
kubectl apply -k overlays/staging

# Delete
kubectl delete -k overlays/production

Comparison: Same App, Different Tools

Helm (values approach):

# Prod
helm install my-app ./chart -f values-prod.yaml

# Staging
helm install my-app ./chart -f values-staging.yaml

# The difference lives in values files (key-value pairs)

Kustomize (patch approach):

# Prod
kubectl apply -k overlays/production

# Staging
kubectl apply -k overlays/staging

# The difference lives in patch files (valid YAML fragments)

Kustomize patches are valid YAML that is merged — easier to review in PRs than template files.


GitOps with ArgoCD

Both work with ArgoCD, but Kustomize integrates more naturally:

ArgoCD with Kustomize:

# ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-production
spec:
  source:
    repoURL: https://github.com/myorg/infra
    path: k8s/overlays/production    # Just point to directory
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

ArgoCD with Helm:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-production
spec:
  source:
    repoURL: https://github.com/myorg/infra
    path: helm/my-app
    targetRevision: main
    helm:
      valueFiles:
        - values-production.yaml
      parameters:
        - name: image.tag
          value: "2.4.1"

Using Both Together

A common pattern: use Helm to deploy third-party apps, Kustomize to patch them:

# kustomization.yaml — patch a Helm-deployed chart
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

# Post-render hook in Helm (pipe Helm output through Kustomize)
resources: []

patches:
  - target:
      kind: Deployment
      name: nginx-ingress-controller
    patch: |
      - op: replace
        path: /spec/replicas
        value: 3

ArgoCD supports Helm + Kustomize post-rendering natively.


When to Choose Each

Choose Helm:

  • Installing third-party apps (PostgreSQL, Prometheus, cert-manager)
  • Distributing your own app as a reusable chart
  • Teams who want values files over patch files
  • Need release history and rollback commands
  • Complex conditional logic in configuration

Choose Kustomize:

  • Pure GitOps workflow with ArgoCD or Flux
  • Prefer plain YAML that’s easy to diff and review
  • Managing per-environment overlays for your own apps
  • Want zero templating logic in configuration files
  • Built into kubectl (no extra CLI to install)

Bottom Line

Helm and Kustomize are complementary, not competing: use Helm to install community-maintained charts (it’s the only practical way to get PostgreSQL or cert-manager), and use Kustomize to manage environment-specific configuration for your own applications. Teams shouldn’t feel forced to pick one — the most effective setup uses both where each shines.