Kustomize already gives us many convenient ways to customize Kubernetes manifests without copying and modifying the original YAML files. We can add name prefixes and suffixes, assign namespaces, apply labels and annotations, update image tags, change replica counts, and generate ConfigMaps and Secrets.
Those features are great for broad, cross-cutting changes. But they are not always precise enough.
At some point, we need to change one specific object in the base, or even one specific field inside one object. For example, we might want to update the image of the main nginx Deployment, while leaving a second nginx-based reverse proxy Deployment untouched. This is where Kustomize patches become important.
Patches allow us to apply targeted changes on top of a base while keeping the base reusable and clean.
What problem do patches solve?
Imagine a base with two Deployments. Both use the nginx image:
base/
├── kustomization.yaml
├── nginx-deployment.yaml
└── reverse-proxy-deployment.yaml
The first Deployment is our main application:
# base/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27.0
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
The second Deployment is a reverse proxy:
# base/reverse-proxy-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: reverse-proxy
labels:
app: reverse-proxy
spec:
replicas: 1
selector:
matchLabels:
app: reverse-proxy
template:
metadata:
labels:
app: reverse-proxy
spec:
containers:
- name: nginx
image: nginx:1.27.0
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
The base includes both resources:
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- nginx-deployment.yaml
- reverse-proxy-deployment.yaml
Now we create a development overlay:
overlays/
└── dev/
└── kustomization.yaml
A first attempt might be to use the top-level images field:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: nginx
newTag: 1.27.1
This works, but it works too broadly. Kustomize will find every image named nginx and change the tag to 1.27.1. In our example, both Deployments are updated:
image: nginx:1.27.1
That is not what we want if only the main nginx Deployment should be changed. The images transformer is excellent when we intentionally want to update all matching image references. It is not the right tool when we need to update only one object.
For that, we use patches.
The modern patches field
In older Kustomize examples, you may still see these fields:
patchesStrategicMerge:
- patch.yaml
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: nginx
path: patch.yaml
For modern Kustomize usage, prefer the unified top-level patches field instead:
patches:
- path: patch.yaml
The patches field can handle both Strategic Merge patches and JSON 6902 patches. It can load patches from files, or it can define them inline in the kustomization.yaml file.
This gives us one consistent mechanism for targeted changes.
Strategic Merge patches
A Strategic Merge patch looks like a partial Kubernetes manifest. Instead of repeating the entire Deployment, we only write the fields we want to change.
Here is an inline patch that changes the image only for the Deployment named nginx:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.27.1
The important parts are:
| Field | Why it matters |
|---|---|
apiVersion | Tells Kustomize which API schema the patch is based on. |
kind | Tells Kustomize which resource kind to patch. |
metadata.name | Identifies the specific object to patch. |
spec.template.spec.containers | Contains only the container fields we want to modify. |
After running the overlay, the main Deployment gets the new image:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.27.1
The reverse proxy Deployment still uses the original image:
apiVersion: apps/v1
kind: Deployment
metadata:
name: reverse-proxy
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.27.0
That is the main advantage of a patch: it targets the resource we actually want to change.
How Strategic Merge handles containers
The containers field is a list. At first, it might look like the patch should replace the whole list, but Strategic Merge is smarter for built-in Kubernetes types.
For Pod containers, Kubernetes uses name as the merge key. That means Kustomize looks for an existing container with the same name.
If the container exists, the patch updates only the fields we provided:
containers:
- name: nginx
image: nginx:1.27.1
If the container does not exist, the patch appends a new container to the list.
For example, this patch updates the existing nginx container and adds a new busybox container:
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.27.1
- name: busybox
image: busybox:1.36.1
command:
- sh
- -c
- "sleep 3600"
The resulting Pod template contains both containers:
containers:
- name: nginx
image: nginx:1.27.1
- name: busybox
image: busybox:1.36.1
command:
- sh
- -c
- "sleep 3600"
This behavior is very useful for environment-specific sidecars. For example, a development overlay could add a debugging sidecar, while the production overlay keeps the base unchanged.
Be careful, however: not every list behaves like containers. Some Kubernetes lists are merged by a key, while others are replaced. The behavior depends on the Kubernetes API field definition. A common example is tolerations, which is usually replaced rather than merged.
Merging nested maps such as resources
Strategic Merge also works well for nested maps.
Suppose the base defines both requests and limits:
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
Now the overlay wants to update only the memory request:
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
memory: "128Mi"
The final result keeps the other fields and updates only the value we specified:
resources:
requests:
cpu: "50m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "128Mi"
If we also update the memory limit, only that nested value is changed:
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
memory: "128Mi"
limits:
memory: "256Mi"
Final result:
resources:
requests:
cpu: "50m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
This is exactly why Strategic Merge patches are so practical. We can describe the final values we care about without rewriting the full object.
Inline patches versus patch files
Inline patches are useful for small examples and quick changes:
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
But inline patches can become hard to maintain as an overlay grows. A real project might need separate changes for resources, images, environment variables, volumes, probes, security context, and annotations.
A more maintainable approach is to move each patch into its own file:
overlays/dev/
├── kustomization.yaml
├── update-resources.patch.yaml
├── use-latest-tag.patch.yaml
└── mount-db-init.patch.yaml
Then reference those files from kustomization.yaml:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: update-resources.patch.yaml
- path: use-latest-tag.patch.yaml
- path: mount-db-init.patch.yaml
Using the .patch.yaml suffix makes the intent clear. It also helps maintainers understand that the file is not a full Kubernetes resource, but a partial patch document.
Example: patching resource requests and limits from a file
Create a patch file:
# overlays/dev/update-resources.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi"
Reference it:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: update-resources.patch.yaml
Build the overlay:
kubectl kustomize overlays/dev
Or, if you use the standalone binary:
kustomize build overlays/dev
The Deployment is updated only where the patch says it should be updated.
Example: patching only the image tag
Create a dedicated patch:
# overlays/dev/use-latest-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:latest
Add it to the overlay:
patches:
- path: update-resources.patch.yaml
- path: use-latest-tag.patch.yaml
This is more precise than using the global images field when multiple resources use the same image name.
However, do not spread conflicting changes across multiple patch files. If two patches update the same field, the result depends on patch order.
Patch order matters
Kustomize applies patches in the order they are listed.
For example:
patches:
- path: use-latest-tag.patch.yaml
- path: use-stable-tag.patch.yaml
If both files update the same container image, the second patch wins.
# use-latest-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:latest
# use-stable-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:stable
With this order:
patches:
- path: use-latest-tag.patch.yaml
- path: use-stable-tag.patch.yaml
The final image is:
image: nginx:stable
With the reverse order:
patches:
- path: use-stable-tag.patch.yaml
- path: use-latest-tag.patch.yaml
The final image is:
image: nginx:latest
This can become confusing quickly. If two patches touch the same field, it is usually better to combine them into one patch so the final intended state is obvious.
Mounting a generated ConfigMap with a patch
Patches are also useful when an overlay needs to mount a generated ConfigMap or Secret into an existing Deployment.
Suppose the development overlay generates a ConfigMap from a database initialization script:
overlays/dev/
├── db-init.sql
├── kustomization.yaml
└── mount-db-init.patch.yaml
Example script:
-- overlays/dev/db-init.sql
CREATE TABLE IF NOT EXISTS example_items (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
Generate a ConfigMap in the overlay:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
configMapGenerator:
- name: db-init-config
files:
- db-init.sql
patches:
- path: mount-db-init.patch.yaml
Now patch the existing nginx Deployment to mount this ConfigMap:
# overlays/dev/mount-db-init.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
volumeMounts:
- name: db-config
mountPath: /db-config
volumes:
- name: db-config
configMap:
name: db-init-config
When Kustomize builds the overlay, the generated ConfigMap may receive a hash suffix:
apiVersion: v1
kind: ConfigMap
metadata:
name: db-init-config-9mt2f7768c
The Deployment reference is also updated:
volumes:
- name: db-config
configMap:
name: db-init-config-9mt2f7768c
This is important. We reference the generator name, db-init-config, in the patch. Kustomize then rewrites the reference to the generated name.
When Strategic Merge is not enough
Strategic Merge patches are excellent for adding or changing fields. But they are not always the best tool for removing fields or performing very exact operations.
For example, suppose we want to remove the entire resources section from the first container in every Deployment:
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
A Strategic Merge patch describes desired values, but removal is more naturally expressed as an operation:
remove this exact path
For that, we can use JSON 6902 patches.
JSON 6902 patches
JSON 6902 patches use a list of operations. Each operation has an op and a path. Some operations also have a value or from field.
Common operations are:
| Operation | Purpose |
|---|---|
add | Add a value at a path. |
remove | Remove a value at a path. |
replace | Replace a value at a path. |
move | Move a value from one path to another. |
copy | Copy a value from one path to another. |
test | Assert that a value matches before continuing. |
In Kustomize, JSON 6902 patches are especially useful when we need exact path-based changes.
Removing resources from a container
Create a JSON 6902 patch file:
# overlays/dev/remove-resources.patch.yaml
- op: remove
path: /spec/template/spec/containers/0/resources
Then reference it with a target:
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: remove-resources.patch.yaml
target:
group: apps
version: v1
kind: Deployment
name: nginx
The target tells Kustomize which object should receive the JSON patch:
| Target field | Example | Meaning |
|---|---|---|
group | apps | API group. For apps/v1, the group is apps. |
version | v1 | API version. |
kind | Deployment | Resource kind. |
name | nginx | Resource name. |
The path uses JSON Pointer syntax:
/spec/template/spec/containers/0/resources
This path means:
spec
└── template
└── spec
└── containers
└── 0
└── resources
After the patch is applied, the resources field is removed from the first container.
YAML or JSON patch files
JSON 6902 patches can be written as YAML:
- op: remove
path: /spec/template/spec/containers/0/resources
Or as JSON:
[
{
"op": "remove",
"path": "/spec/template/spec/containers/0/resources"
}
]
Both represent the same patch document. YAML is usually easier to read in Kubernetes projects, but JSON can be useful if you want to stay close to the original RFC 6902 format.
Why JSON 6902 paths can be brittle
JSON 6902 patches are precise, but that precision comes with a trade-off.
This path targets the first container:
/spec/template/spec/containers/0/resources
If someone changes the container order in the base, the patch might remove resources from the wrong container.
For example, imagine the base changes from this:
containers:
- name: nginx
image: nginx:1.27.0
resources:
requests:
cpu: "50m"
memory: "64Mi"
To this:
containers:
- name: busybox
image: busybox:1.36.1
- name: nginx
image: nginx:1.27.0
resources:
requests:
cpu: "50m"
memory: "64Mi"
The JSON path /spec/template/spec/containers/0/resources now points to busybox, not nginx.
This is why Strategic Merge is usually better when you can identify list items by merge keys such as container name. JSON 6902 is best when you truly need exact path-level operations, especially removal or replacement.
Applying one JSON patch to multiple resources
The target field does not always need a resource name. We can target a broader group.
For example, this removes the first container’s resources field from every Deployment in the overlay:
patches:
- path: remove-resources.patch.yaml
target:
group: apps
version: v1
kind: Deployment
This applies to all Deployments, but not to Pods, Services, ConfigMaps, or Secrets.
If the base has these resources:
base/
├── nginx-deployment.yaml
├── reverse-proxy-deployment.yaml
└── reverse-proxy-pod.yaml
And the target is:
target:
group: apps
version: v1
kind: Deployment
Then the patch applies to:
nginx-deployment.yaml
reverse-proxy-deployment.yaml
But it does not apply to:
reverse-proxy-pod.yaml
Even if the standalone Pod has a similar resources field, it will not be touched because the target kind is Deployment.
Targeting with label selectors
Targets can also use label selectors.
For example, we can patch only Deployments with the label tier=edge:
patches:
- path: remove-resources.patch.yaml
target:
group: apps
version: v1
kind: Deployment
labelSelector: tier=edge
A matching Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: reverse-proxy
labels:
app: reverse-proxy
tier: edge
A non-matching Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
tier: app
This makes selectors useful when several resources need the same modification, but only if the patch is compatible with every selected resource.
For example, this is risky:
target:
labelSelector: environment=dev
If the selector matches Deployments, Services, and ConfigMaps, a patch path like /spec/template/spec/containers/0/resources will fail for objects that do not have that path.
A safer target is more specific:
target:
group: apps
version: v1
kind: Deployment
labelSelector: environment=dev
Targeting with annotation selectors
Annotation selectors work similarly:
patches:
- path: remove-resources.patch.yaml
target:
group: apps
version: v1
kind: Deployment
annotationSelector: patching.example.com/remove-resources=true
Example resource:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
annotations:
patching.example.com/remove-resources: "true"
Annotation selectors are useful when you do not want to use labels for patching logic. Labels often have operational meaning for selectors, services, network policies, or monitoring. An annotation can be a cleaner way to mark resources for customization.
Strategic Merge versus JSON 6902
Both patch styles are useful, but they solve different problems.
| Use case | Prefer | Why |
|---|---|---|
| Update a Deployment image by container name | Strategic Merge | More readable and less dependent on container order. |
| Add a sidecar container | Strategic Merge | Can append a new container while preserving existing containers. |
| Update nested resource requests or limits | Strategic Merge | Merges maps cleanly. |
| Remove a specific field | JSON 6902 | remove expresses deletion clearly. |
| Replace an exact path | JSON 6902 | Precise path-based operation. |
| Patch several resources selected by label | Either | Use target.labelSelector, but ensure every match supports the patch. |
| Patch CRDs | Often JSON 6902 | Do not assume built-in Strategic Merge behavior for custom resources. |
A good default is:
- Use Strategic Merge when you want to describe partial Kubernetes YAML.
- Use JSON 6902 when you need exact operations such as
remove,replace, or path-specificadd.
Using labels instead of deprecated commonLabels
You may still see older examples using commonLabels:
commonLabels:
environment: dev
In modern Kustomize, prefer the labels field:
labels:
- pairs:
environment: dev
includeSelectors: false
includeTemplates: true
This matters because labels can affect selectors. Accidentally changing selectors can break Services, Deployments, NetworkPolicies, or other resources that depend on label matching.
The labels field gives you more control:
| Field | Meaning |
|---|---|
pairs | Labels to add. |
includeSelectors | Whether to add labels to selectors. Use carefully. |
includeTemplates | Whether to add labels to Pod templates. |
For patching examples, labels are also useful because they can be used as patch targets:
target:
kind: Deployment
labelSelector: environment=dev
A complete overlay example
Here is a complete development overlay that demonstrates several patterns together:
overlays/dev/
├── db-init.sql
├── kustomization.yaml
├── mount-db-init.patch.yaml
├── update-resources.patch.yaml
└── use-latest-tag.patch.yaml
The kustomization.yaml file:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namePrefix: dev-
labels:
- pairs:
environment: dev
includeSelectors: false
includeTemplates: true
configMapGenerator:
- name: db-init-config
files:
- db-init.sql
patches:
- path: update-resources.patch.yaml
- path: use-latest-tag.patch.yaml
- path: mount-db-init.patch.yaml
The resource patch:
# update-resources.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi"
The image patch:
# use-latest-tag.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:latest
The ConfigMap mount patch:
# mount-db-init.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
volumeMounts:
- name: db-config
mountPath: /db-config
volumes:
- name: db-config
configMap:
name: db-init-config
Build it:
kubectl kustomize overlays/dev
Apply it:
kubectl apply -k overlays/dev
This overlay keeps the base intact while applying development-specific changes in small, readable patch files.
Practical best practices
Use patches intentionally. They are powerful, but they can also make overlays hard to understand if everything becomes a patch.
A good patching style looks like this:
- Keep the base clean and reusable.
- Use top-level Kustomize fields for broad changes.
- Use patches for resource-specific changes.
- Prefer small patch files that do one thing.
- Use clear names such as
update-resources.patch.yamlormount-db-init.patch.yaml. - Avoid multiple patches changing the same field.
- Prefer Strategic Merge for normal Kubernetes object changes.
- Use JSON 6902 for exact operations such as removing fields.
- Be careful with JSON 6902 array indexes like
/containers/0. - Use
kind,name,labelSelector, andannotationSelectorto avoid patching unintended resources. - Run
kubectl kustomize <overlay>before applying changes. - Review the final rendered YAML, not only the patch files.
Common mistakes
Using images when only one resource should change
The images field updates all matching image names. This is useful for global image updates, but not for precise object-level changes.
Use a patch if only one Deployment should change.
Forgetting to add a new resource to the base
If you create a new file such as reverse-proxy-deployment.yaml, remember to add it to the base kustomization.yaml:
resources:
- nginx-deployment.yaml
- reverse-proxy-deployment.yaml
Kustomize only processes resources that are part of the kustomization graph.
Assuming all lists merge by name
The containers list has a merge key, but not all lists behave that way. Some lists are replaced. Always check the rendered output.
Relying too much on JSON Patch array indexes
A path like this is fragile:
/spec/template/spec/containers/0/resources
It depends on container order. If the order changes, the patch can affect the wrong container.
Selecting too many resources
This target is broad:
target:
labelSelector: environment=dev
This target is safer:
target:
group: apps
version: v1
kind: Deployment
labelSelector: environment=dev
The more specific target is less likely to match incompatible resources.
Conclusion
Kustomize patches are the tool we reach for when general transformations are not enough.
Top-level fields such as images, replicas, labels, namePrefix, and configMapGenerator are useful for common customization patterns. But patches give us fine-grained control over specific resources and fields.
Strategic Merge patches are usually the best starting point because they look like normal Kubernetes YAML and can merge fields intelligently for built-in Kubernetes types. JSON 6902 patches are more surgical and are especially useful when we need to remove or replace exact paths.
The key is to keep patches small, explicit, and well-targeted. When used carefully, they make Kustomize overlays powerful without turning the base into duplicated YAML.
References
- Kubernetes: Declarative Management of Kubernetes Objects Using Kustomize - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/kustomization/
- Kubernetes: Update API Objects in Place Using kubectl patch - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/
- Kubernetes: kubectl patch Reference - kubernetes[.]io/docs/reference/kubectl/generated/kubectl_patch/
- Kustomize API Types: Kustomization Fields - pkg[.]go[.]dev/sigs.k8s.io/kustomize/api/types
- Kustomize: Inline Patch Example - github[.]com/kubernetes-sigs/kustomize/blob/master/examples/inlinePatch.md
- Kustomize: Patching Multiple Resources Example - github[.]com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md
- IETF: RFC 6902 JSON Patch - datatracker[.]ietf[.]org/doc/html/rfc6902