The controller you'll actually use — declare how many replicas of a Pod you want, and it keeps them running, replaces dead ones, and rolls out new versions.
Start from a clean default namespace. If an old web Deployment exists from a previous run, delete it first:
kubectl delete -f manifests/core-objects/web-deployment.yaml --ignore-not-foundA bare Pod has no safety net: if it crashes hard or its node dies, nothing brings it back. You also can't scale it, and updating its image means deleting and recreating it by hand — with downtime.
A Deployment fixes all of that. You declare a desired state — "run 3 replicas of this Pod template" — and a controller continuously works to make it true. This is the declarative model in action:
- Self-healing — a Pod (or whole node) dies → the Deployment creates a replacement to get back to 3.
- Scaling — change
replicas: 3toreplicas: 5and the cluster converges. - Rolling updates — change the image and Pods are replaced gradually, with zero downtime (see Rolling Update & Rollback).
Analogy: a Deployment is like a thermostat. You set a target — "3 replicas" — and it constantly checks the room and nudges reality back toward that number. Individual Pods come and go; the target is what you manage, not each Pod by hand.
Under the hood a Deployment manages a ReplicaSet, which manages the Pods. You almost always work at the Deployment level.
Deployment ──manages──▶ ReplicaSet ──manages──▶ Pods
This Deployment runs three nginx replicas. The selector must match the Pod template's labels — that's how the Deployment knows which Pods are "its".
▶ Runnable manifest: manifests/core-objects/web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web # ← must match template.metadata.labels below
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27 # pin a version — never rely on :latest
ports:
- name: http
containerPort: 80
resources: # best practice: always set requests/limits
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128MiReading it top to bottom:
replicas: 3— how many copies you want running.template:— the blueprint the Deployment stamps out, copy by copy. It's just the Pod spec you already know (without a name — each Pod gets an auto-generated one likeweb-6f8c…-2kx9q).selector.matchLabels— how the Deployment finds the Pods it owns. It must equal the template'slabels, which is whyapp: webappears in both theselectorand thetemplate. Mismatch them and the Deployment won't recognize its own Pods — a classic first-timer trip-up.resources— reserves a little CPU/memory for each Pod; don't fuss over the exact numbers yet (Resource Requests & Limits explains them).
💡 Don't memorize YAML. Generate a starting point and edit it — this is how most people write manifests:
kubectl create deployment web --image=nginx:1.27 --dry-run=client -o yaml > web-deployment.yamlThis example is deliberately minimal. A production Deployment also adds health probes, covered in their own chapter.
Apply it and watch the three layers appear:
kubectl apply -f manifests/core-objects/web-deployment.yaml
kubectl rollout status deployment/web # blocks until all replicas are ready
kubectl get deploy,rs,pods -l app=webNAME READY UP-TO-DATE AVAILABLE
deployment.apps/web 3/3 3 3
NAME DESIRED CURRENT READY
replicaset.apps/web-6f8c… 3 3 3
NAME READY STATUS RESTARTS
pod/web-6f8c…-2kx9q 1/1 Running 0
pod/web-6f8c…-7nv4d 1/1 Running 0
pod/web-6f8c…-q8m2l 1/1 Running 0
This is the single best thing to see rather than read about. Launch k9s:
k9sType :pods ⏎ to list Pods, then delete one and watch it return. In k9s, highlight a web-… Pod and press Ctrl-D (delete) — or from another terminal, delete a single Pod by name:
kubectl delete pod "$(kubectl get pod -l app=web -o name | head -1)"You'll watch that Pod go Terminating and a fresh one appear within seconds — the Deployment noticed it dropped below 3 replicas and reconciled. You never told it to; that's the controller doing its job. (In k9s, press d on a Pod for describe, l for logs.)
No k9s?
kubectl get pods -l app=web -wshows the same thing — the-w(watch) flag streams changes live as the replacement is created.
Two equivalent ways — declarative (preferred, stays in Git) and imperative (quick):
# Declarative: edit replicas: 3 -> replicas: 5 in the manifest, then re-apply
kubectl apply -f manifests/core-objects/web-deployment.yaml
# Imperative: one-off, handy for experiments
kubectl scale deployment/web --replicas=5Watch the new Pods schedule live in k9s. More on automatic scaling in Scaling.
- Pin image tags (
nginx:1.27, notnginx:latest) so rollouts are reproducible. - Always set
resources.requests/limits— the scheduler needs requests to place Pods well; limits stop one Pod starving the node. See Resource Requests & Limits. - Add health probes so Kubernetes knows when a Pod is truly ready/alive. See Health Checks.
- Treat
selectoras immutable — it's fixed after creation; choose stable labels likeapp: web. - Keep Deployments as YAML in version control, not as imperative commands, so cluster state is reproducible.
If you're reading straight through, leave this Deployment running for the next few chapters. ReplicaSet, DaemonSet, Job & CronJob, and especially Service all build on or compare against it.
If you're done experimenting and want to reset the lab, delete it:
kubectl delete -f manifests/core-objects/web-deployment.yaml # removes the Deployment, its ReplicaSet, and all Pods