kubectl apply gets used everywhere as an obvious default, the go-to command for deploying a manifest. What it actually does goes well beyond “apply this file”: it computes a three-way merge between three distinct states, an invisible mechanism whose ignorance explains a classic bug — the manual edit that vanishes without warning.

Three states, not two

kubectl apply compares the state currently stored in the cluster, the last applied configuration (kept in an annotation), and the file being applied right now:

# The kubectl.kubernetes.io/last-applied-configuration annotation
# stores exactly what the last apply sent
kubectl get deployment my-app -o jsonpath='{.metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration}'

kubectl create or kubectl replace only know the file provided: they overwrite or create, with no memory of the past. kubectl apply alone keeps that third reference, which lets it detect a field was removed from the file since the last apply and delete it on the cluster, not just add what’s new.

The classic trap: the kubectl edit that vanishes

A manual change via kubectl edit, never reflected back into the source file, survives as long as nobody reruns kubectl apply on that same file. On the next apply, the three-way merge compares the file (unchanged) to the last applied configuration (also unchanged): the manually-edited field, absent from both, gets treated as if it was never supposed to exist, and silently disappears, no warning, no confirmation.

$ kubectl edit deployment my-app
# replicas bumped from 3 to 5 by hand, in a hurry

$ kubectl apply -f deployment.yaml
# the source file still says 3: apply silently reverts to 3

This isn’t a kubectl apply bug: it’s the expected behavior of a declarative tool that assumes the source file is the only truth. The real bug lives elsewhere, in the habit of editing a cluster by hand without reflecting the change back into the file that will eventually get reapplied, exactly the drift risk the infrastructure as code article covers for Terraform, carried over to Kubernetes.

Server-side apply: the merge moves to the API server

Classic (“client-side”) apply computes the merge locally, inside kubectl, before sending the result. Server-side apply, enabled via --server-side, moves that computation into the API server itself, with a concrete benefit: field ownership, where the server tracks who last wrote which field.

kubectl apply --server-side -f deployment.yaml

Two different controllers (an operator managing spec.replicas based on a metric, a GitOps deployment managing everything else) can then coexist on the same object without stepping on each other, each owning its own fields. Client-side apply, older and still the default without the flag, makes no ownership distinction: whichever apply runs last wins, on the entire object.

What stays the same either way

The base principle doesn’t change between client-side and server-side: the source file remains the only truth the tool knows about. A GitOps pipeline continuously reconciling from Git relies entirely on that assumption: any change bypassing Git, manual or not, is doomed to disappear on the next reconciliation cycle, exactly like a kubectl edit disappears on the next apply.

Takeaway

kubectl apply merges three states (cluster, last applied config, current file), not two, which explains why a manual change never reflected back into the source file silently vanishes on the next deploy. That’s not a bug, it’s the direct consequence of a declarative tool treating the file as the only truth. Server-side apply adds field ownership, letting several controllers coexist on the same object without conflict, a foundation that matters the moment a Kubernetes migration introduces multiple actors touching the same resources.