Kustomize lets you customize Kubernetes YAML without templates. You write plain manifests once in a base, then describe what changes per environment (namespace, replicas, image tag, resource limits, configuration) in small overlays. Kustomize is built into kubectl, so kubectl apply -k works out of the box. In this tutorial you will build a base for a web application, create development and production overlays, apply patches and generated ConfigMaps and Secrets, and deploy both environments to a cluster.

Prerequisites

To follow this guide you need:

  • A Kubernetes cluster you can deploy to, for example k3s on a CubePath VPS or a local kind cluster.
  • kubectl 1.27 or newer, configured for that cluster. These versions embed Kustomize v5, which supports every field used here.
  • Basic familiarity with Deployments and Services.

Step 1 - Checking or installing Kustomize

kubectl already includes Kustomize. Check which version it embeds:

kubectl version --client
Client Version: v1.33.x
Kustomize Version: v5.x.x

That is enough for the whole tutorial. If you also want the standalone kustomize binary, which usually ships newer features first and adds the kustomize edit commands used in Step 7, download the official install script, review it, and run it:

curl -fsSLO https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh
less install_kustomize.sh
bash install_kustomize.sh
sudo install -m 0755 kustomize /usr/local/bin/kustomize
kustomize version
v5.x.x

In the rest of the guide, kubectl kustomize <dir> and kustomize build <dir> are interchangeable: both print the rendered manifests without touching the cluster.

Step 2 - Creating the base

The base holds the manifests every environment shares. Create the directory layout:

mkdir -p ~/web-app/base ~/web-app/overlays/development ~/web-app/overlays/production
cd ~/web-app

The final layout will look like this:

PathContents
base/Deployment, Service and the base kustomization.yaml
overlays/development/Development namespace, image tag and name prefix
overlays/production/Production namespace, replicas, limits, config and secrets

Create the Deployment:

nano base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 128Mi

Create the Service:

nano base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-app
spec:
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 80

Now create the kustomization.yaml that lists the resources and adds a common label to all of them:

nano base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml

labels:
  - pairs:
      app.kubernetes.io/name: web-app
      app.kubernetes.io/managed-by: kustomize

The labels field replaces the deprecated commonLabels. By default it only adds labels to metadata.labels and leaves selectors untouched, which avoids breaking existing Deployments whose selectors are immutable.

Render the base to check it:

kubectl kustomize base

The output shows both resources with the two new labels in their metadata.

Step 3 - Creating the development overlay

An overlay points to the base in its resources list and then changes it. The older bases field is deprecated; use resources for bases too.

nano overlays/development/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

namespace: development
namePrefix: dev-

labels:
  - pairs:
      environment: development

images:
  - name: nginx
    newTag: 1.27-alpine

Each field does one job:

  • namespace sets the namespace on every namespaced resource.
  • namePrefix renames every resource and updates references to them, so the Service still selects the right Pods.
  • images changes the tag of every container that uses the nginx image, without a patch.

Render it and check the result:

kubectl kustomize overlays/development | grep -E "name: dev-|namespace:|image:"
  name: dev-web-app
  namespace: development
  name: dev-web-app
  namespace: development
        image: nginx:1.27-alpine

Step 4 - Creating the production overlay with patches

Production needs more replicas and higher limits. Patches describe only the fields that change. Kustomize supports two kinds.

A strategic merge patch looks like a partial manifest. Kustomize matches it to the base resource by kind and name and merges it:

nano overlays/production/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: web-app
          resources:
            requests:
              cpu: 200m
              memory: 128Mi
            limits:
              memory: 256Mi

A JSON 6902 patch lists exact operations on paths. It is useful when a merge is ambiguous, for example to add a single item to a list. Here it can go inline in the kustomization.

Create the production kustomization with both patches:

nano overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

namespace: production

labels:
  - pairs:
      environment: production

images:
  - name: nginx
    newTag: "1.27"

patches:
  - path: deployment-patch.yaml
  - target:
      kind: Deployment
      name: web-app
    patch: |-
      - op: add
        path: /spec/template/spec/containers/0/readinessProbe
        value:
          httpGet:
            path: /
            port: 80
          periodSeconds: 10

Quote tags such as "1.27" so YAML does not read them as numbers. Render the overlay and confirm the patches applied:

kubectl kustomize overlays/production | grep -E "replicas:|readinessProbe:|memory:"
  replicas: 3
        readinessProbe:
            memory: 256Mi
            memory: 128Mi

Step 5 - Generating ConfigMaps and Secrets

Generators build ConfigMaps and Secrets from literals or files and append a hash of the content to the name, for example web-app-config-5g7k9mb2tf. Kustomize updates every reference to that name, so changing the configuration changes the Pod template and triggers a rolling update.

Create an environment file with non-sensitive settings and another one for secrets:

nano overlays/production/app.env
LOG_LEVEL=warn
MAX_CONNECTIONS=200
nano overlays/production/secret.env
API_KEY=your_api_key

Keep secret.env out of Git (add it to .gitignore) and create it from your secret store at deploy time. Add the generators to the end of overlays/production/kustomization.yaml:

configMapGenerator:
  - name: web-app-config
    envs:
      - app.env

secretGenerator:
  - name: web-app-secrets
    envs:
      - secret.env

Reference them from the Deployment by their base names. Add this block under the web-app container in overlays/production/deployment-patch.yaml, at the same level as resources:

          envFrom:
            - configMapRef:
                name: web-app-config
            - secretRef:
                name: web-app-secrets

Render the overlay and check that the names now carry the hash in both places:

kubectl kustomize overlays/production | grep -E "web-app-(config|secrets)"
  name: web-app-config-5g7k9mb2tf
  name: web-app-secrets-8hc4d2k7m9
            name: web-app-config-5g7k9mb2tf
            name: web-app-secrets-8hc4d2k7m9

The first two lines are the generated objects and the last two are the references inside the Deployment.

Step 6 - Deploying the overlays

Namespaces are not created by the namespace field, so create them first:

kubectl create namespace development
kubectl create namespace production

Preview what production would change in the cluster, then apply it:

kubectl diff -k overlays/production
kubectl apply -k overlays/production
configmap/web-app-config-5g7k9mb2tf created
secret/web-app-secrets-8hc4d2k7m9 created
service/web-app created
deployment.apps/web-app created

Apply development the same way and compare both environments:

kubectl apply -k overlays/development
kubectl get deployments -A -l app.kubernetes.io/name=web-app
NAMESPACE     NAME          READY   UP-TO-DATE   AVAILABLE   AGE
development   dev-web-app   1/1     1            1           10s
production    web-app       3/3     3            3           40s

Change LOG_LEVEL in app.env, run kubectl apply -k overlays/production again and watch the Deployment roll out new Pods with kubectl rollout status deployment/web-app -n production. That is the hash suffix at work.

Step 7 - Updating image tags from CI

In a pipeline you normally change only the image tag of one overlay. With the standalone binary, kustomize edit rewrites kustomization.yaml for you:

cd ~/web-app/overlays/production
kustomize edit set image nginx=nginx:1.27.5
grep -A2 "images:" kustomization.yaml
images:
- name: nginx
  newTag: 1.27.5

A CI job then either commits this change so a GitOps tool such as Argo CD or Flux deploys it, or runs kubectl apply -k overlays/production directly followed by kubectl rollout status to fail the job if the rollout does not complete.

Troubleshooting

accumulating resources ... no such file or directory. A path in resources or patches is wrong. Paths are relative to the directory of the kustomization.yaml that lists them.

A strategic merge patch does nothing or fails with no matches for Id. The patch's kind and metadata.name must match the resource in the base, using the name before any namePrefix or nameSuffix is applied.

Warnings about deprecated fields. Kustomize v5 still accepts bases, commonLabels, patchesStrategicMerge and patchesJson6902 but warns about them. Run kustomize edit fix in the directory to migrate the file to resources, labels and patches.

field is immutable when applying. A label change reached spec.selector of an existing Deployment. Use labels without includeSelectors: true, or delete and recreate the Deployment if you really need the new selector.

Conclusion

You built a Kustomize base for a web application, customized it for development and production with overlays, strategic merge and JSON 6902 patches, image overrides and hashed ConfigMaps and Secrets, and deployed both environments with kubectl apply -k. Next, store the repository in Git and let Argo CD or Flux apply each overlay, add a components directory for optional features you want to reuse across overlays, or combine Kustomize with Helm charts through the helmCharts field.