A Kubernetes object rarely has a single writer. The YAML file that created a Deployment is just one of many things that change it — controllers, GitOps tools, Helm, admission webhooks, an HPA, and the occasional kubectl edit all touch the same object. Server-Side Apply (SSA) moves the merge logic and field ownership into the API server so those writers stop silently clobbering each other. This guide explains the old model, why it fails, how SSA fixes it, and how to adopt it safely.
A Deployment is shared state. All of these can legitimately modify it:
spec.replicasWhen two of them touch the same field with different values, the question is: who owns it? Classic kubectl apply had no real answer — and that gap causes silent, surprising overwrites.
kubectl apply (the default before SSA) computes a three-way merge from three inputs:
last-applied-configuration — an annotation kubectl stores on the object recording what it last applied.kubectl diffs these locally and sends a strategic merge patch to the API server. It works well — until another tool changes a field without updating kubectl's annotation.
kubectl apply -f deploy.yaml # classic, client-side merge
Say your manifest declares replicas: 3, and an HPA later scales the live object to 5:
# last-applied-configuration (annotation kubectl trusts)
replicas: 3
---
# live object after the HPA acted
spec:
replicas: 5 # the HPA's decision
The HPA never touched kubectl's annotation, which still says replicas: 3. So your next kubectl apply computes "3 → 3, no change intended by me" and quietly patches replicas back to 3 — undoing the autoscaler, with no conflict and no warning. That silent revert is exactly the class of bug SSA was built to eliminate.
With --server-side, kubectl stops merging locally. Instead it sends your fully specified intent to the API server as content type application/apply-patch+yaml, tagged with a field manager name. The API server does the merge, using the object's schema and recorded field ownership.
kubectl apply --server-side -f deploy.yaml
# kubectl → API server:
# PATCH /apis/apps/v1/namespaces/default/deployments/my-app
# Content-Type: application/apply-patch+yaml
# fieldManager=kubectl
kubectl fetches the OpenAPI schema (/openapi/v3/...) so the server knows how to merge lists and maps structurally — but it does not need to GET the live object or keep a last-applied annotation. The API server is the single source of truth for who owns what.
managedFieldsSSA records ownership in the object's metadata.managedFields. Each entry says which manager owns which fields, via a field map (fieldsV1) that lists keys but omits values — the presence of a key means "this manager owns this field."
managedFields:
- manager: kubectl
operation: Apply
fieldsV1:
f:spec:
f:replicas: {} # kubectl owns spec.replicas
- manager: k3s
operation: Update
subresource: status # the controller owns status
This metadata tells you exactly which manager changed which field, and when — turning "who set this?" from guesswork into a lookup.
kubectl get deploy my-app --show-managed-fields -o yaml | yq '.metadata.managedFields'
Because ownership is tracked, SSA can surface conflicts instead of hiding them. If Helm applies a manifest that changes .spec.replicas, but kubectl already owns that field with a different value, the API server rejects the apply:
kubectl apply --server-side --field-manager=helm -f deployment-5-replicas.yaml
# error: Apply failed with 1 conflict: conflict with "kubectl":
# .spec.replicas
The error names the field and its current owner, so you immediately know which manager is contesting ownership. You then choose deliberately:
kubectl apply --server-side --field-manager=helm --force-conflicts -f deployment-5-replicas.yaml
replicas from Git when the HPA owns it).That explicit choice is the whole point: no more silent reverts.
x-kubernetes-list-type: map/set and merge keys), SSA treats a CRD's lists atomically — the whole list is replaced on apply instead of merged element by element. Well-authored CRDs set these so co-ownership of list items works.patch, update, or subresource write still overwrites an SSA-owned field without raising an SSA conflict. SSA only guards the apply path, so mixing imperative edits can still surprise you.Reach for SSA when multiple controllers or tools manage the same objects and you need the API server to:
replicas).For a simple object with a single writer, classic apply is fine. The moment an HPA, a GitOps controller, and Helm all touch one Deployment, SSA is what keeps them from fighting in the dark.
replicas in Git while an HPA is active. Either drop it from the manifest (let the HPA own it) or accept that every apply fights the autoscaler. With SSA you'll at least see the conflict.--force-conflicts reflexively. It silently seizes ownership — the very thing SSA is meant to make deliberate. Read the conflict first.kubectl edit/patch with apply. Those bypass SSA's conflict detection and muddy managedFields.kubectl apply --server-side -f deploy.yaml, then inspect metadata.managedFields — which manager owns spec.replicas?--field-manager and a different replicas value. Read the conflict error.--force-conflicts (take ownership) and once by removing the field (yield). Compare managedFields after each.kubectl apply -f and watch replicas get reverted. Repeat with --server-side and observe the conflict instead.Self-check:
{} under a field key mean?--force-conflicts do, and why is it a deliberate choice?The takeaway: classic apply guesses who owns a field and sometimes guesses wrong, silently. Server-Side Apply makes ownership explicit and conflicts loud — which is exactly what you want when many tools share one object.
Learned the concepts? Test yourself with Kubernetes interview questions.
Go to the question bank →