Managing Kubernetes manifests is easy when an application has one environment and only a few resources. A Deployment, a Service, maybe a ConfigMap, and everything looks manageable.
The complexity appears when the same application needs to run in multiple environments:
- Development may need one replica and a debug-friendly image tag.
- Staging may need production-like settings but smaller resource limits.
- Production may need more replicas, stricter labels, and a different namespace.
- Some environments may need extra resources, while others should not include them.
Without a configuration tool, the common approach is often to copy the YAML files and change a few values. That works for a short time, but it creates duplication. When the base Deployment changes, every copied version must be updated manually. This is exactly the kind of configuration drift that makes Kubernetes environments harder to maintain.
Kustomize solves this problem with a simple but powerful model: bases and overlays.
A base contains the shared Kubernetes resources. An overlay references that base and applies environment-specific changes on top of it. The base remains untouched, while each overlay generates the final manifests for a specific environment.
What Kustomize Does
Kustomize is a Kubernetes-native configuration management tool. It works with plain YAML manifests and does not require a separate templating language. Instead of writing templates with variables, conditionals, and loops, you define normal Kubernetes objects and then describe how they should be transformed.
You can use Kustomize in two common ways:
kubectl kustomize <kustomization-directory>
This renders the final YAML to the terminal.
kubectl apply -k <kustomization-directory>
This applies the rendered resources to the cluster.
The important part is that kubectl apply -k expects a directory, not a single YAML file. That directory must contain a kustomization.yaml file.
The Core Idea: Base Plus Overlay
A base is the reusable foundation of your application configuration. It usually contains resources that are common across environments:
- Deployment
- Service
- ConfigMap
- Secret references
- ServiceAccount
- Ingress
- HorizontalPodAutoscaler
- NetworkPolicy
An overlay is another Kustomize directory that points to the base and adds changes for a specific environment. Typical overlay changes include:
- Namespace
- Replica count
- Image tag
- Labels and annotations
- Resource requests and limits
- Environment-specific ConfigMaps
- Ingress hostnames
- Feature-specific patches
The relationship looks like this:
base resources + dev overlay = final dev manifests
base resources + prod overlay = final prod manifests
The base does not know anything about the overlays. This is important. A clean base should stay environment-neutral unless the application is truly bound to one environment by design.
Recommended Directory Structure
A common structure is to keep one application folder, one base folder, and one folder per environment under overlays.
nginx-app/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── dev-replicas.yaml
└── prod/
├── kustomization.yaml
└── prod-replicas.yaml
This is not mandatory. Kustomize does not force a specific folder structure. However, this structure is simple, readable, and works well in Git repositories and GitOps workflows.
Creating the Base
Start with the base Deployment. This should be the shared version of the application. Do not put environment-specific values here unless they are genuinely common to all environments.
Create nginx-app/base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/part-of: nginx-app
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: nginx
template:
metadata:
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/part-of: nginx-app
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
Now create the Service.
Create nginx-app/base/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/part-of: nginx-app
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: nginx
ports:
- name: http
port: 80
targetPort: 80
Finally, create the base kustomization.yaml.
Create nginx-app/base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
At this point, the base can be rendered directly:
kubectl kustomize nginx-app/base
This prints the Deployment and Service to the terminal. Nothing is applied to the cluster yet.
Creating the Dev Overlay
The dev overlay references the base and adds development-specific configuration.
Create nginx-app/overlays/dev/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
- ../../base
patches:
- path: dev-replicas.yaml
images:
- name: nginx
newTag: "1.27"
Create nginx-app/overlays/dev/dev-replicas.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
Now render the dev overlay:
kubectl kustomize nginx-app/overlays/dev
The output will contain the base Deployment and Service, but with the dev namespace applied.
Creating the Prod Overlay
Production can use the same base, but apply different settings.
Create nginx-app/overlays/prod/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
- ../../base
patches:
- path: prod-replicas.yaml
images:
- name: nginx
newTag: "1.27"
Create nginx-app/overlays/prod/prod-replicas.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
Render the production overlay:
kubectl kustomize nginx-app/overlays/prod
The same base is now used to generate production manifests, but with a different namespace and replica count.
Important Namespace Detail
The namespace field in kustomization.yaml sets the namespace on namespaced resources. It does not automatically create the Namespace object in the cluster.
This means the following overlay configuration:
namespace: dev
will add metadata.namespace: dev to resources such as Deployments and Services, but the dev Namespace must already exist.
You can create namespaces separately:
kubectl create namespace dev
kubectl create namespace prod
Or you can manage namespace objects declaratively:
apiVersion: v1
kind: Namespace
metadata:
name: dev
Whether namespace YAML belongs inside the overlay depends on ownership. If the namespace is part of the application lifecycle, include it in the overlay. If namespaces are managed centrally by the platform team, keep them outside the application Kustomize tree.
Applying the Overlays
After creating the namespaces, apply the dev overlay:
kubectl apply -k nginx-app/overlays/dev
Apply the production overlay:
kubectl apply -k nginx-app/overlays/prod
Check the resources:
kubectl get pods -n dev
kubectl get svc -n dev
kubectl get pods -n prod
kubectl get svc -n prod
You should see one NGINX Deployment in the dev namespace and another NGINX Deployment in the prod namespace. Both came from the same base manifests.
Preview Before Applying
One of the most useful Kustomize habits is to render before applying:
kubectl kustomize nginx-app/overlays/prod
You can also compare the desired state with the current cluster state:
kubectl diff -k nginx-app/overlays/prod
This is especially useful in CI/CD pipelines and GitOps workflows because it makes the final generated YAML reviewable.
Deleting Resources
Kustomize also works with kubectl delete -k.
kubectl delete -k nginx-app/overlays/dev
kubectl delete -k nginx-app/overlays/prod
This deletes the resources generated by each overlay. If Namespace objects are managed outside the overlay, they will not be deleted by this command.
What Actually Happens During a Kustomize Build
When you run:
kubectl kustomize nginx-app/overlays/prod
Kustomize performs a build process:
- It reads the overlay
kustomization.yaml. - It follows the
resourcespath to the base. - It loads the resources declared by the base.
- It applies overlay-level transformations, such as
namespace,images, labels, annotations, and patches. - It emits the final YAML.
The base files are not modified. The result exists only as rendered output unless you apply it to the cluster.
This is the key difference between Kustomize and manual copying. You keep one shared source of truth and generate environment-specific output from it.
Customizing with Patches
Patches are one of the most important parts of Kustomize. They let you change selected fields without duplicating the whole resource.
A patch can be small and focused. For example, this patch changes only the replica count:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
This patch does not need to repeat the entire Deployment. Kustomize matches it to the Deployment named nginx and merges the specified fields.
Strategic Merge Style Patch
For native Kubernetes resources such as Deployments, strategic merge style patches are often readable and convenient.
Example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
This patch changes the resource requests and limits for the container named nginx.
You can reference it from the overlay:
patches:
- path: prod-resources.yaml
JSON 6902 Style Patch
Sometimes you need a more precise patch. JSON 6902 patches are useful when you want explicit operations such as add, replace, or remove.
Example:
patches:
- target:
group: apps
version: v1
kind: Deployment
name: nginx
patch: |-
- op: replace
path: /spec/replicas
value: 3
This is more verbose, but also very explicit. It is useful when patching arbitrary fields, custom resources, or fields where strategic merge behavior is not suitable.
Customizing Image Tags
Changing image tags is such a common operation that Kustomize has a dedicated images field.
Example:
images:
- name: nginx
newTag: "1.27.4"
You can also rewrite the image registry or repository:
images:
- name: nginx
newName: registry.example.com/platform/nginx
newTag: "1.27.4"
This is cleaner than patching the full container spec when the only thing you need to change is the image reference.
For production, avoid relying on floating tags such as latest. Prefer immutable version tags or digests so that the same Git revision always produces a predictable deployment.
Adding Labels and Annotations
Overlays are also useful for cross-cutting metadata.
Example:
labels:
- pairs:
environment: prod
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
commonAnnotations:
owner: platform-team
Be careful with labels that affect selectors. Changing selectors on Deployments or Services can have operational consequences. For environment labels, it is often safer to apply them to resources and pod templates without automatically changing selectors.
Multiple Bases
An overlay can reference more than one base.
resources:
- ../../base
- ../../../shared/monitoring-sidecar
- ../../../shared/network-policy
This can be useful when you have shared platform resources that should be composed with an application.
However, use multiple bases carefully. If every environment pulls in too many shared layers, it can become harder to understand where a field comes from. Keep the structure simple enough that a reviewer can still follow the generated output.
Bases, Overlays, and GitOps
Kustomize fits naturally with GitOps tools because the generated state is fully declarative.
A GitOps controller such as Argo CD can point directly to an overlay path:
nginx-app/overlays/prod
The controller renders that overlay and applies the resulting resources to the cluster. This keeps the Git repository clean:
- The base contains common application manifests.
- The overlay contains environment-specific intent.
- The cluster receives fully rendered Kubernetes resources.
This also makes promotion easier. For example, a team can promote a new version by updating the image tag in the staging overlay, testing it, and then applying the same image tag to the production overlay.
When to Use Bases and Overlays
Bases and overlays are a good fit when you have mostly similar environments with predictable differences.
Good use cases include:
- Same application deployed to dev, staging, and production.
- Same service deployed to multiple clusters.
- Different image tags per environment.
- Different replica counts per environment.
- Environment-specific Ingress hosts.
- Optional production-only resource limits or autoscaling.
Kustomize may be less comfortable when you need heavy conditional logic, loops, or complex value interpolation. In those cases, Helm or another templating system may be a better fit. The strength of Kustomize is that it keeps the YAML close to normal Kubernetes YAML.
Best Practices
Keep the base environment-neutral. A base should usually not contain namespace: dev, production-only annotations, or environment-specific replica counts.
Keep patches small. A patch that changes one thing is easier to review than a patch that changes replicas, image tags, environment variables, probes, and resource limits at the same time.
Prefer the modern patches field. Older examples often use fields such as patchesStrategicMerge or patchesJson6902. The unified patches field is clearer for new configurations.
Render before applying. Use kubectl kustomize and kubectl diff -k as part of your normal workflow.
Do not hide too much in overlays. If a patch becomes almost as large as the original resource, consider whether the base is too generic or whether the environment really needs its own resource definition.
Be intentional with labels and selectors. Automatically changing selectors can break relationships between Services, Deployments, and Pods if not handled carefully.
Avoid generated output in Git unless you have a specific reason. Usually, Git should store the source manifests and overlays, not the rendered YAML.
Common Mistakes
| Mistake | Why it causes problems | Better approach |
|---|---|---|
| Copying full YAML files per environment | Creates drift and repeated maintenance | Keep common resources in base and patch differences in overlays |
| Putting namespaces in the base | Makes the base less reusable | Set namespace in each overlay |
Expecting namespace: to create a Namespace | Kustomize only sets the namespace field on resources | Create Namespace objects separately or include them as resources |
| Using large patches | Makes reviews difficult | Split patches by concern |
| Applying without rendering | Hides the final state | Run kubectl kustomize or kubectl diff -k first |
| Using floating image tags in production | Makes deployments less predictable | Use fixed tags or digests |
Complete Example
Final directory tree:
nginx-app/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── dev-replicas.yaml
│ └── kustomization.yaml
└── prod/
├── prod-replicas.yaml
└── kustomization.yaml
Render dev:
kubectl kustomize nginx-app/overlays/dev
Render prod:
kubectl kustomize nginx-app/overlays/prod
Apply dev:
kubectl apply -k nginx-app/overlays/dev
Apply prod:
kubectl apply -k nginx-app/overlays/prod
Delete dev:
kubectl delete -k nginx-app/overlays/dev
Delete prod:
kubectl delete -k nginx-app/overlays/prod
Conclusion
Kustomize bases and overlays provide a clean way to manage Kubernetes environments without duplicating YAML. The base acts as the reusable source of truth, while overlays describe what needs to change for each environment.
This model is simple, but it scales well. You can start with a Deployment and a Service, then gradually add patches, image overrides, metadata, ConfigMaps, Secrets, Ingress resources, and GitOps integration.
The main discipline is to keep the base clean and the overlays focused. When each overlay clearly describes its environment-specific differences, Kubernetes configuration becomes easier to review, easier to promote, and easier to maintain.
References
- Kubernetes: Declarative Management of Kubernetes Objects Using Kustomize - kubernetes[.]io/docs/tasks/manage-kubernetes-objects/kustomization/
- Kustomize: Kubernetes native configuration management - kustomize[.]io/
- Kubernetes SIG CLI: Kustomize GitHub repository - github[.]com/kubernetes-sigs/kustomize
- Argo CD: Kustomize user guide - argo-cd.readthedocs[.]io/en/stable/user-guide/kustomize/