Skip to content
Go back

Kustomize Without Helm: When Overlays Win

By SumGuy 8 min read
Kustomize Without Helm: When Overlays Win

You Don’t Need Helm

Here’s the thing: Helm is to Kubernetes what a luxury SUV is to off-roading — technically more capable, but if you just need to move a family of five to the grocery store, you’re paying for features that’ll never leave the parking lot.

For home labs running a handful of services on k3s, Helm introduces complexity you don’t need. Templating languages, value files, chart dependencies, repository management — it all adds friction. Meanwhile, Kustomize sits there, quiet, doing exactly one thing well: letting you manage Kubernetes manifests without templating syntax at all.

The secret sauce? Overlays. Base manifests + layer-specific customizations = clean separation, no Helm, no headaches.

Kustomize: The Overlay Pattern

Kustomize is built into kubectl (since 1.14). No additional tools. No dependencies. Just a kustomization.yaml file that tells kubectl how to patch, merge, and combine your YAML manifests.

The core idea:

No variables, no templating conditionals. You write real Kubernetes YAML. Overlays apply patches on top.

Think of it like a photo editor: the base is your original image, overlays are adjustment layers that change colors or crop without destroying the original.

Directory Structure

Here’s what a typical home lab setup looks like:

my-app/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
├── overlays/
│ ├── dev/
│ │ └── kustomization.yaml
│ ├── staging/
│ │ └── kustomization.yaml
│ └── prod/
│ └── kustomization.yaml
└── .gitignore

Small, obvious, no magic. Everyone on your team (even if it’s just you) knows what goes where.

Base Manifests

Your base is straightforward YAML — nothing special. Here’s a simple app:

base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 8080
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: log_level
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: webapp
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
selector:
app: webapp
base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
log_level: "info"
base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
commonLabels:
app: webapp
managed-by: kustomize

That’s it. The base kustomization.yaml just lists what files to include and applies common labels everywhere.

Overlays: Dev, Staging, Prod

Now the magic. Dev needs 1 replica, higher log level, smaller resources. Prod needs 3 replicas, info-only logs, more memory. Staging sits between.

Kustomize patches let you change specific fields without duplicating the entire manifest.

overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
replicas:
- name: webapp
count: 1
patches:
- target:
kind: Deployment
name: webapp
patch: |-
- op: replace
path: /spec/template/spec/containers/0/env/0/value
value: "debug"
- op: replace
path: /spec/template/spec/containers/0/resources/requests/memory
value: 64Mi
configMapGenerator:
- name: app-config
behavior: merge
literals:
- log_level=debug
overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
replicas:
- name: webapp
count: 2
configMapGenerator:
- name: app-config
behavior: merge
literals:
- log_level=info
overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
replicas:
- name: webapp
count: 3
patches:
- target:
kind: Deployment
name: webapp
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources/requests/memory
value: 256Mi
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: 1Gi
configMapGenerator:
- name: app-config
behavior: merge
literals:
- log_level=warn

See what happened? Each overlay specifies only the differences. Dev changes replicas and debug mode. Prod ups the resource requests. Zero duplication. Change the base once, all overlays inherit it.

Building & Deploying

To preview what Kustomize will generate:

Terminal window
kubectl kustomize overlays/dev

To apply directly:

Terminal window
kubectl apply -k overlays/prod

That -k flag is your friend. It tells kubectl “this is a Kustomization, build it and apply it.”

For GitOps workflows (ArgoCD, Flux), you point your Application or Kustomization resource at the overlay path:

argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: webapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/you/my-app
targetRevision: main
path: overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true

ArgoCD syncs whatever the kustomization produces. One Application resource per overlay (dev points to overlays/dev, prod points to overlays/prod), and you’re golden.

JSON Patches vs Strategic Merge Patches

That patch: |- stuff above is a JSON patch — it’s explicit but verbose if you’re changing multiple fields. Kustomize also supports strategic merge patches, which are more concise:

overlays/dev/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 1
template:
spec:
containers:
- name: app
resources:
requests:
memory: 64Mi

Then reference it:

overlays/dev/kustomization.yaml
patches:
- path: deployment-patch.yaml

Kustomize merges this onto the base Deployment, keeping fields you didn’t specify unchanged. Much cleaner for multi-field changes.

When Kustomize Wins (and When It Doesn’t)

Kustomize shines for:

Helm is still better for:

For a home lab? Stick with Kustomize.

Real Example: Multi-Replica App with Ingress

Let’s build something closer to reality — a web app with an Ingress for external access:

myapp/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── configmap.yaml
├── overlays/
│ ├── dev/
│ │ ├── kustomization.yaml
│ │ └── ingress-patch.yaml
│ └── prod/
│ ├── kustomization.yaml
│ └── hpa.yaml

Base Ingress (simple):

base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webapp-ingress
spec:
ingressClassName: traefik
rules:
- host: myapp.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: webapp
port:
number: 80

Dev overlay changes the hostname and skips TLS:

overlays/dev/ingress-patch.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webapp-ingress
spec:
rules:
- host: myapp-dev.localhost

Prod overlay adds TLS and a real domain:

overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patches:
- target:
kind: Ingress
name: webapp-ingress
patch: |-
- op: replace
path: /spec/rules/0/host
value: myapp.example.com
- op: add
path: /spec/tls
value:
- secretName: myapp-tls
hosts:
- myapp.example.com
resources:
- hpa.yaml

And hpa.yaml (only in prod):

overlays/prod/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: webapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: webapp
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70

Now:

Base is untouched. Changes cascade automatically.

The Decision: Kustomize or Helm?

Ask yourself:

  1. Are you deploying a Helm chart from someone else? → Use Helm, you have no choice.
  2. Are you running a home lab with <20 services? → Kustomize. Full stop.
  3. Do you have non-engineer team members installing your app? → Helm charts are more polished for distribution.
  4. Are you managing 50+ services with complex cross-service configuration? → Helm’s centralized values (or Kustomize with a generator framework) becomes attractive.
  5. Is your team already comfortable with Helm? → Keep it, switching is friction.

For everyone else, especially home lab folks tired of Helm’s learning curve? Kustomize is the right tool. You get environment-specific manifests, GitOps-friendly output, and zero templating nonsense.

Your 2 AM self — the one debugging why a variable didn’t interpolate correctly — will thank you.

Further Reading


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts