← Learn··Updated 31 May 2026·5 min read

Deployments, ReplicaSets, and self-healing

How a Deployment owns a ReplicaSet that owns pods, why that chain gives you self-healing, and how rolling updates and one-command rollbacks fall out of the same model.

Kubernetes
Kubernetes from scratch with k3sPart 5 of 9
#kubernetes
#k3s
#deployments
#ai-assisted

The last part ended on an uncomfortable fact: a bare pod that dies stays dead. Nothing watches it, nothing replaces it, and a single node reboot wipes it out. That is exactly the gap a Deployment fills. A Deployment is the object you reach for ninety-nine times out of a hundred, because it turns "I ran a pod once" into "I want this many healthy copies running, forever, and here is how to upgrade them without downtime." This part is about the three-layer chain that makes that work, and the two operations — rolling updates and rollback — that fall straight out of it.

level:Intermediate verified:Jun 2026

ℹ️ Read-along — concepts, with small examples you can try on your k3s cluster if you have one, but nothing here is required setup.

What a Deployment actually is

A Deployment is a controller that manages a stateless workload declaratively. You do not tell it how to get to a state; you write down the desired state — which image, how many replicas, what the pods should look like — and the Deployment controller continuously works to make the live cluster match. That declarative loop is the whole point. The manifest is the source of truth, and the cluster reconciles toward it.

Crucially, a Deployment does not manage pods directly. It manages a ReplicaSet, and the ReplicaSet manages the pods. That extra layer looks redundant until you see how updates work — it is the seam the rolling update uses.

The chain: Deployment to ReplicaSet to Pods

The hierarchy is three links long, and each link has exactly one job:

  • The Deployment holds the desired image and replica count, and owns the update strategy. It is what you edit.
  • The ReplicaSet has one job: keep a fixed number of identical pods running. It is a counting controller — "I should see three pods matching this template; I see two; create one."
  • The Pods are the actual running containers, as covered in the previous part.
flowchart TB
  D["Deployment<br/><i>desired image + replicas: 3</i>"]
  R["ReplicaSet<br/><i>keep 3 pods alive</i>"]
  P1["Pod 1"]
  P2["Pod 2"]
  P3["Pod 3"]
  D -->|"owns + updates"| R
  R -->|"maintains count"| P1
  R -->|"maintains count"| P2
  R -->|"maintains count"| P3

You edit the Deployment. It manages a ReplicaSet, which does nothing but keep the right number of identical pods running. The pods do the work.

💡 Tip You almost always deploy a Deployment, not a pod. A bare pod has no controller behind it; a Deployment gives you replicas, self-healing, rolling updates, and rollback for free. Treat the bare pod as a debugging tool only.

Self-healing: the ReplicaSet never stops counting

Self-healing is not a special feature you switch on — it is just the ReplicaSet doing its one job in a loop. The controller's desired state says "three pods"; if it observes two, the difference is a signal, and it creates a replacement. Kill a pod by hand, lose it to a crash, or drain the node it sits on, and within seconds a new pod appears to restore the count.

# Create a Deployment imperatively (kubectl run only makes bare pods now)
kubectl create deployment web --image=nginx --replicas=3

# Watch the controller maintain the count: delete a pod and a new one is born
kubectl get pods -w
kubectl delete pod <one-of-the-web-pods>
# ... the ReplicaSet immediately schedules a replacement

# Change the desired count and the ReplicaSet reconciles
kubectl scale deployment web --replicas=5

This is the difference the previous part promised. A bare pod is a one-shot; a Deployment records intent, and intent is what survives a node reboot. When the node comes back — or another node picks up the slack — the count is restored because the desired state never changed.

ℹ️ Note Scaling is just editing the desired count. kubectl scale deployment web --replicas=5 changes one number; the ReplicaSet notices the gap and creates two more pods. Nothing is "restarted" — the existing three are untouched.

Rolling updates: where the second ReplicaSet earns its keep

Now the two-layer indirection pays off. When you change the pod template — bump the image tag, say — the Deployment does not mutate the existing ReplicaSet. It creates a new ReplicaSet for the new template and then gradually scales the new one up while scaling the old one down. Old pods keep serving traffic until new pods are ready, so there is no gap. This is a rolling update, and it is the default strategy.

Two knobs govern the pace. maxSurge is how many pods above the desired count may exist during the roll (extra new pods spun up ahead of time); maxUnavailable is how many below the desired count are tolerated (old pods torn down early). Together they let you trade rollout speed against spare capacity.

flowchart LR
  subgraph OLD["Old ReplicaSet &mdash; v1"]
    O1["pod v1"]
    O2["pod v1"]
    O3["pod v1"]
  end
  subgraph NEW["New ReplicaSet &mdash; v2"]
    N1["pod v2"]
    N2["pod v2"]
    N3["pod v2"]
  end
  OLD -->|"scale down<br/><i>maxUnavailable</i>"| X["zero downtime<br/><i>traffic always served</i>"]
  NEW -->|"scale up<br/><i>maxSurge</i>"| X

A rolling update is two ReplicaSets overlapping: the new one grows as the old one shrinks. Because old pods keep serving until new ones are Ready, the service never goes dark.

# Trigger a rolling update by changing the image
kubectl set image deployment/web nginx=nginx:1.27

# Watch the roll happen and block until it finishes (or fails)
kubectl rollout status deployment/web
⚠️ Warning A rolling update is only zero-downtime if your pods report readiness honestly. Without a readiness probe, Kubernetes considers a container "ready" the instant it starts, and may route traffic to a pod that is still warming up. Define readiness probes before you rely on rolling updates in production.

Rollback: the old ReplicaSet is still there

Because the old ReplicaSet is scaled to zero rather than deleted, the Deployment keeps a revision history — so you can roll back at any time. A rollback is just the rolling-update machinery run in reverse: scale the previous ReplicaSet back up, scale the current one down.

# See the revision history
kubectl rollout history deployment/web

# Undo the last rollout (back to the previous revision)
kubectl rollout undo deployment/web

# Or pin to a specific earlier revision
kubectl rollout undo deployment/web --to-revision=2

The whole workflow is declarative, so the file you actually keep in Git is the manifest below. Everything above — scaling, rolling, rolling back — is the cluster reconciling toward this, applied with kubectl apply -f.

web-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80

Save it and kubectl apply -f web-deployment.yaml, then confirm the controller built the chain:

# The Deployment reports its desired/ready replica count
kubectl get deploy web
# NAME   READY   UP-TO-DATE   AVAILABLE   AGE
# web    3/3     3            3           20s

# And three pods exist, each owned by the Deployment's ReplicaSet
kubectl get pods
# NAME                   READY   STATUS    RESTARTS   AGE
# web-5d4f8b7c9b-2xk7p   1/1     Running   0          20s
# web-5d4f8b7c9b-8wq4n   1/1     Running   0          20s
# web-5d4f8b7c9b-lp9rt   1/1     Running   0          20s
💡 Tip kubectl apply -f deploy.yaml is the command you should reach for, not create. apply is idempotent and declarative — re-running it after editing the file reconciles the cluster to the new spec, which is exactly the GitOps-friendly habit you want.
Danger Do not kubectl edit or kubectl scale a Deployment in a cluster whose manifests live in Git. The next apply (or your GitOps controller) will overwrite the live change and you will lose it silently. Edit the file, commit, then apply — keep one source of truth.

A short close

A Deployment is three layers doing one job each: the Deployment holds your intent and the update strategy, the ReplicaSet keeps the pod count exactly right, and the pods run the containers. Self-healing, scaling, zero-downtime rolling updates, and one-command rollback all fall out of that same reconcile-to-desired-state loop — you never script any of it. Next, those healthy pods need a stable address that survives every roll and reschedule, because pod IPs come and go. That is Services and networking. If you want to revisit what a pod is underneath the controller, see pods explained, and the series hub maps the rest of the path.