A Kubernetes Deployment manifest is a description of the pods you want, how many of them, and how to replace them safely, and roughly a third of the fields people copy into it do nothing useful.
The examples that circulate are usually the minimum viable manifest plus whatever the author happened to have in their cluster. That is fine for a demo and it is why so many production Deployments have no resource limits, no probes, and a rolling update strategy nobody chose.
This walks the manifest field by field, with an opinion about each: what it does, what happens when you leave it out, and whether you should care.
Table of contents
- The minimum that actually works
- Resource requests and limits are not optional
- Probes: the difference between readiness and liveness
- Rolling update strategy, and what maxUnavailable really means
- Graceful shutdown, which almost nobody gets right
- Whether you need any of this
- How this fits the rest of the stack
- FAQ
The minimum that actually works
Here is a Deployment with nothing decorative in it. Every field below is load-bearing.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: registry.example.com/web:1.4.2
ports:
- containerPort: 8080
Two things about this that trip people up.
First, spec.selector.matchLabels must match spec.template.metadata.labels. They are two separate places that say the same thing, and the API server will reject a mismatch. Worse, the selector is immutable after creation, so getting it wrong means deleting and recreating the Deployment.
Second, that image tag should never be latest. A Deployment decides whether to roll by comparing the pod template, and if the template is byte-identical nothing happens. Push a new image under the same tag and Kubernetes sees no change, so your deploy silently does nothing. Tag with a version or a commit SHA.
Resource requests and limits are not optional
Leave these out and the scheduler assumes your pod needs nothing, packs it onto a node with no headroom, and your service gets throttled or evicted at the worst time.
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
The distinction matters. Requests are what the scheduler reserves, and they decide which node you land on. Limits are the hard ceiling the kernel enforces.
Memory and CPU behave completely differently at the limit. Exceed a memory limit and the container is killed outright, which shows up as an OOMKilled restart. Exceed a CPU limit and the container is throttled, which shows up as mysterious latency with no error anywhere.
That difference is why the manifest above sets a memory limit and no CPU limit. A memory limit protects the node from a leak. A CPU limit throttles a service that could have used idle capacity, and it produces tail latency that is genuinely difficult to trace back to its cause. Set CPU requests so the scheduler places you correctly, and think hard before adding a CPU limit.
Probes: the difference between readiness and liveness
These two look similar and do opposite things, and confusing them is how a slow dependency turns into a restart loop.
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 20
failureThreshold: 6
Readiness controls traffic. Fail it and the pod is removed from the Service endpoints but keeps running. This is the one you always want: it is what makes a rolling update safe, because a new pod takes no traffic until it says it is ready.
Liveness controls restarts. Fail it and the kubelet kills the container. This is the dangerous one. If your liveness endpoint checks the database and the database has a bad minute, every replica fails liveness at once and Kubernetes restarts your entire fleet during an incident, turning a degraded service into an outage.
The rule that avoids this: liveness should test only whether the process itself is wedged. Readiness can check dependencies. Liveness should not. And if you are unsure whether you need a liveness probe at all, you probably do not.
A startupProbe is the right tool for a slow-booting application, rather than inflating initialDelaySeconds on liveness until it covers the worst case.
Rolling update strategy, and what maxUnavailable really means
The default strategy replaces pods gradually, which is usually right. The two knobs decide how aggressively.
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
maxUnavailable: 0 with maxSurge: 1 means: never drop below the desired replica count, add one extra pod at a time, wait for it to be ready, then retire an old one. Slower, and it keeps full capacity throughout. This is what you want for anything user-facing.
The defaults are 25% for both, which means a three-replica Deployment can run on two pods during a rollout. If two pods cannot carry your traffic, your rollout is a small outage that you scheduled yourself.
One more field that earns its place:
minReadySeconds: 10
progressDeadlineSeconds: 600
revisionHistoryLimit: 5
minReadySeconds stops a pod that passes its probe and immediately crashes from being counted as a success, which is what makes a broken rollout march all the way through your fleet. progressDeadlineSeconds marks the rollout failed rather than hanging forever.
Graceful shutdown, which almost nobody gets right
When a pod is terminated, two things happen at the same time: it is removed from the Service endpoints, and it receives SIGTERM. Those are independent and racy, and the race is why you see a handful of failed requests on every deploy.
Endpoint removal propagates through kube-proxy and any ingress controller, and that takes a moment. Meanwhile your process already got the signal and started shutting down. Requests that were routed just before the removal arrive at a process that is closing.
The fix is a small sleep before shutdown begins:
lifecycle:
preStop:
exec:
command: ["sleep", "10"]
terminationGracePeriodSeconds: 40
The preStop hook runs before SIGTERM. Sleeping there gives endpoint removal time to propagate while the pod keeps serving normally. Then SIGTERM arrives, your application finishes in-flight requests and exits, and nothing is dropped.
Make sure the grace period comfortably exceeds the preStop sleep plus your longest request. If it does not, the kubelet sends SIGKILL mid-request and you are back where you started.
Whether you need any of this
It is worth saying plainly: a Deployment manifest is a small part of running Kubernetes. Around it sit a Service, an Ingress, cluster upgrades, a CNI plugin, storage classes, RBAC, secret management, and a control plane that somebody patches.
That whole apparatus earns its keep when you have many services, several teams, genuine multi-tenancy, or a workload that needs custom scheduling. For a single application with a database, it is a large amount of machinery around a problem that is mostly solved by pushing a repository and getting a URL back.
The honest test: list what you are getting from Kubernetes that a managed platform would not give you. If the list is rolling updates, health checks and horizontal scaling, those come as configuration on most platforms rather than as a cluster. On RunxBuild that is autoscaling between a floor and a ceiling plan, a build log, a live route and a rollback to the previous deploy, and the autoscaling documentation covers the scaling half.
If the list includes operators, custom resources or scheduling constraints you can actually name, Kubernetes is the right answer and the fields above are the ones to get right.
How this fits the rest of the stack
The cost of a cluster is rarely the cluster. It is the control plane fee plus the nodes plus the load balancer plus the storage plus the person who understands all of it, and that total is worth comparing against the simpler shape before committing. The RunxBuild hosting calculator prices the simpler shape, which makes the comparison concrete rather than architectural.
Useful related references:
- Serverless Kubernetes: When It Works, When It Doesn’t, When to Pick It
- Kubernetes Backup: An etcd Snapshot Is Not a Backup of Your App
- Kubernetes Volumes: PV, PVC, StorageClass, and the Right Pattern
- Services on RunxBuild
FAQ
Why did my deployment not roll out after I pushed a new image?
Because you reused the same tag. Kubernetes compares the pod template to decide whether to roll, and an identical template means no change. Tag images with a version or commit SHA so the manifest genuinely differs. Using the latest tag makes this failure mode permanent.
What is the difference between a readiness probe and a liveness probe?
Readiness controls whether the pod receives traffic; failing it removes the pod from the Service while leaving it running. Liveness controls restarts; failing it kills the container. Readiness may check dependencies, liveness should only check whether the process itself is wedged.
Should I set CPU limits on my containers?
Usually not. Exceeding a CPU limit throttles the container rather than killing it, which produces latency with no error to trace. Set CPU requests so the scheduler places the pod correctly, set a memory limit so a leak cannot take down the node, and add a CPU limit only when you specifically need to cap a noisy workload.
What does maxUnavailable do during a rolling update?
It sets how many pods may be missing from the desired count while the rollout runs. The default of 25% means a three-replica deployment can drop to two pods mid-rollout. Setting it to zero with maxSurge of one keeps full capacity throughout, at the cost of a slower rollout.
Why do I see failed requests during every deployment?
Endpoint removal and SIGTERM happen concurrently, so requests routed just before removal arrive at a process already shutting down. Add a preStop hook that sleeps for around ten seconds before termination begins, and make sure terminationGracePeriodSeconds exceeds that sleep plus your longest request.