Deployments in k0s - Managing Applications at Scale
Status: Active
Last Updated: 2026-08-14
Category: Containers - Core k0s
Prerequisites: k0s-pods-services
Time: 3-4 hours
Tags: kubernetes, k0s, deployments, replicas, rolling-update, rollback, health-checks
Summary
Stop creating pods by hand. Deployments are Kubernetes' answer to "how do I run N copies of my app, update them without downtime, and undo a bad release?" This lesson covers the ReplicaSet machinery underneath, scaling up and down, zero-downtime rolling updates, instant rollbacks, and liveness/readiness probes that make updates safe.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Explain the Deployment โ ReplicaSet โ Pod hierarchy
- โ Create and scale Deployments declaratively
- โ Perform zero-downtime rolling updates
- โ Roll back a bad release with one command (and know how it works)
- โ Configure liveness, readiness, and startup probes correctly
- โ Understand surge/unavailable settings to tune rollout speed
- โ Troubleshoot stuck rollouts
๐๏ธ Why Not Just Use Pods?
In k0s-pods-services you created pods directly. Here's what that costs you in production:
| Problem with raw Pods | What a Deployment does |
|---|---|
| Pod dies โ stays dead | Automatically recreated |
| Node dies โ workload gone until you act | Rescheduled onto healthy nodes |
| Update = delete + create (downtime) | Rolling update, always โฅ N available |
| Bad release โ manual fix | One-command rollback |
| Need 3 copies โ write 3 YAML files | replicas: 3, done |
The Ownership Chain
A Deployment doesn't manage pods directly. There's an intermediate layer:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Deployment: web โ
โ "I want 3 replicas matching app=web โ
โ running template vX" โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ creates & owns
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ReplicaSet: web-7d9f8c6b5 โ
โ "I want exactly 3 pods from this template" โ
โ (hash of template = revision identity) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ creates & owns
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโ
โผ โผ โผ
Pod web-... Pod web-... Pod web-...
Why ReplicaSets exist: each ReplicaSet corresponds to one specific pod template โ its name ends in a hash of the template. When you update a Deployment, it creates a new ReplicaSet for the new template, scales it up while scaling the old one down. The old ReplicaSet isn't deleted (up to revisionHistoryLimit) โ that's exactly what makes rollbacks possible: just scale the old ReplicaSet back up.
Verify this yourself later:
sudo k0s kubectl get rs -l app=web
You'll see one active ReplicaSet per pod-template revision.
๐ Your First Deployment
deployment.yaml
apiVersion: apps/v1 # note: apps group, not core v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3 # desired count - the whole point
selector:
matchLabels:
app: web # MUST match template labels below
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # may exceed replicas by 1 during rollout
maxUnavailable: 0 # never drop below replicas during rollout
template: # โ the pod template (compare: kind: Pod)
metadata:
labels:
app: web # selector must match these
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 96Mi
readinessProbe: # is it ready to receive traffic?
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe: # should we restart it?
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # same label - Service finds all 3 pods
ports:
- port: 80
targetPort: 80
Apply and inspect:
sudo k0s kubectl apply -f deployment.yaml
sudo k0s kubectl get deployments
sudo k0s kubectl get pods -l app=web
Output:
NAME READY UP-TO-DATE AVAILABLE AGE
web 3/3 3 3 45s
NAME READY STATUS RESTARTS AGE
web-7d9f8c6b5-kx2mz 1/1 Running 0 45s
web-7d9f8c6b5-p9vrt 1/1 Running 0 45s
web-7d9f8c6b5-tq4ld 1/1 Running 0 45s
Notice pod names now carry the ReplicaSet hash (7d9f8c6b5) plus a random suffix โ ownership is visible right in the name.
What Happens on apply:
1. API server stores Deployment object in etcd
2. Deployment controller notices desired=3, current=0
3. Creates ReplicaSet web-7d9f8c6b5 (hash of pod template)
4. ReplicaSet controller sees desired=3, current=0
5. Creates 3 pods from the template
6. Scheduler assigns each pod to a node
7. Kubelets pull image and start containers
8. Endpoints controller adds ready pods to Service "web"
Self-Healing: Try to Break It
This is the demo that makes people love Deployments:
# Kill a pod
sudo k0s kubectl delete pod web-7d9f8c6b5-kx2mz
# Immediately watch
sudo k0s kubectl get pods -w
web-7d9f8c6b5-kx2mz 1/1 Terminating 0 5m
web-7d9f8c6b5-nq87h 0/1 Pending 0 0s
web-7d9f8c6b5-nq87h 0/1 ContainerCreating 0 0s
web-7d9f8c6b5-nq87h 1/1 Running 0 3s
The ReplicaSet noticed current=2 < desired=3 and spawned a replacement within seconds. No human involved.
๐ Replica Management
Scaling Declaratively (Preferred)
Change replicas: 3 โ replicas: 5 in your YAML and re-apply:
sudo k0s kubectl apply -f deployment.yaml
Git stays the source of truth (GitOps is Law).
Scaling Imperatively (Quick Experiments)
sudo k0s kubectl scale deployment web --replicas=5
โ ๏ธ Trap: if you later kubectl apply a file still saying replicas: 3, it scales back down. Pick one source of truth per change; imperative scale is fine for load tests, not for production state.
Watching Scale Events
sudo k0s kubectl scale deployment web --replicas=6
sudo k0s kubectl describe deployment web | tail -12
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 30s deployment-controller Scaled up replica set web-7d9f8c6b5 to 6
One event, six pods โ the Deployment only talks to its ReplicaSet.
Resource reality check: each replica reserves its
requests:on some node's capacity. On a small single-node k0s VM,replicas: 10ร 64Mi requests will start failing scheduling โ see the troubleshooting section.
๐ Rolling Updates
The Default Behavior
Update the image tag:
sudo k0s kubectl set image deployment/web nginx=nginx:1.27-bookworm
# or edit the YAML and apply - same result, git-tracked
Watch it happen:
sudo k0s kubectl rollout status deployment/web
Waiting for deployment "web" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "web" rollout to finish: 2 old replicas are pending termination...
deployment "web" successfully rolled out
What Happens During a Rolling Update:
Phase 0: RS-old has 3/3 pods
Phase 1: Deployment creates RS-new, scales it to maxSurge allowance
โ 3 old + 1 new pod running (4 total)
Phase 2: New pod passes readinessProbe
โ added to Service endpoints
โ RS-old scaled down by 1 (maxUnavailable respected)
โ 2 old + 1 new
Phase 3..n: repeat until RS-new has all 3, RS-old scaled to 0
Final: RS-old kept at 0 replicas (rollback insurance)
At every moment, maxSurge: 1, maxUnavailable: 0 guarantees at least 3 healthy pods serve traffic. That's zero-downtime by construction.
Tuning Speed vs Safety
| Strategy | Meaning | When |
|---|---|---|
maxSurge: 1, maxUnavailable: 0 |
Extra capacity, never dip below N | Production default |
maxSurge: 0, maxUnavailable: 1 |
No extra capacity, brief capacity dip | Resource-constrained nodes |
maxSurge: 25%, maxUnavailable: 25% |
Percentage-based, faster | Big fleets, tolerant apps |
type: Recreate |
Kill ALL pods first, then start new | Dev-only or apps that can't run two versions side-by-side (e.g., schema-incompatible DB migrations handled elsewhere) |
Readiness Gates the Rollout
Here's why readiness probes matter so much: a pod that isn't Ready never enters Service endpoints, so users can't hit it while it warms up. If your app takes 20s to warm caches, without a readiness probe users hit a cold instance mid-rollout. With one, the rollout simply waits:
New pod starts โ probe fails โ NOT in endpoints โ rollout pauses
App becomes warm โ probe passes โ added to endpoints โ old pod terminated
Remove the readiness probe and watch rollouts break user traffic โ a classic first-week mistake.
โช Rollbacks
The Magic One-Liner
Bad release? Version 1.27-bookworm crashes under load. Undo everything:
sudo k0s kubectl rollout undo deployment/web
What Happens:
1. Controller finds previous ReplicaSet (the one at 0 replicas)
2. Scales IT back up using the SAME rolling-update strategy
3. Scales the bad ReplicaSet down
4. Result: previous template is live again, no downtime
That's the payoff of keeping old ReplicaSets around.
Rollout History
sudo k0s kubectl rollout history deployment/web
deployment.apps/web
REVISION CHANGE-CAUSE
1 <none>
2 <none>
Pro tip โ annotate releases as you ship them so history is meaningful:
sudo k0s kubectl set image deployment/web nginx=nginx:1.27-perl \
--record=false
sudo k0s kubectl annotate deployment/web \
kubernetes.io/change-cause="switch variant for perl module support"
Roll Back to a Specific Revision
# Jump straight back to revision 1, skipping intermediate ones
sudo k0s kubectl rollout undo deployment/web --to-revision=1
Pause-and-Resume (Canary-style)
You can freeze a rollout partway to inspect the new version with limited blast radius:
sudo k0s kubectl set image deployment/web nginx=nginx:alpine
sudo k0s kubectl rollout pause deployment/web
# ...some new pods live, most traffic still on old version. Inspect!
sudo k0s kubectl rollout resume deployment/web
Note: pause too long (>~5 hours) and the controller times out the reservation โ treat pause as minutes, not days.
๐ฉบ Health Checks: The Three Probes
| Probe | Question asked | Failure action |
|---|---|---|
livenessProbe |
"Are you alive?" | Restart the container |
readinessProbe |
"Ready for traffic?" | Removed from Service endpoints (not restarted) |
startupProbe |
"Started yet? (slow boot)" | Blocks other probes until it passes |
All three support the same check types:
httpGet: { path: /health, port: 8080 } # 2xx/3xx = pass
tcpSocket: { port: 5432 } # can connect = pass
exec: # exit code 0 = pass
command: ["cat", "/tmp/healthy"]
grpc: { port: 9000 } # gRPC health protocol
Getting Them Right
containers:
- name: slow-app
image: myorg/api:2.1
startupProbe: # handles slow boots FIRST
httpGet: { path: /healthz, port: 3000 }
failureThreshold: 30 # 30 x 10s = up to 5 min to start
periodSeconds: 10
readinessProbe: # gates traffic
httpGet: { path: /ready, port: 3000 }
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe: # catches hangs/deadlocks
httpGet: { path: /healthz, port: 3000 }
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
Rules of thumb:
- Liveness checks must be cheap and internal โ no database calls. A DB blip would otherwise restart every pod simultaneously (self-inflicted outage).
- Readiness CAN check dependencies โ being removed from endpoints while the DB is down is correct behavior.
- Different endpoints matter:
/healthz= process alive;/ready= actually able to serve. Don't point both at the same handler unless they truly mean the same thing. - Always give slow-starting apps a
startupProbeโ otherwise liveness kills them before they finish booting (CrashLoopBackOff).
๐ ๏ธ Hands-On Lab: Full Release Lifecycle
Run a complete release cycle โ ship, watch, break, roll back โ against a tiny echo app:
cat <<'EOF' > /tmp/app-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: lab-app
spec:
replicas: 4
selector:
matchLabels: { app: lab }
template:
metadata:
labels: { app: lab }
spec:
containers:
- name: app
image: hashicorp/http-echo:1.0
args: ["-text=v1", "-listen=:5678"]
ports: [{ containerPort: 5678 }]
readinessProbe:
tcpSocket: { port: 5678 }
periodSeconds: 2
EOF
sudo k0s kubectl apply -f /tmp/app-v1.yaml
sudo k0s kubectl rollout status deployment/lab-app
# Ship v2
sudo k0s kubectl set image deployment/lab-app app=hashicorp/http-echo:1.0 \
&& sudo k0s kubectl annotate deployment/lab-app --overwrite \
kubernetes.io/change-cause="release v2"
# (image tag unchanged here; in real life you'd bump it)
# Watch revisions
sudo k0s kubectl rollout history deployment/lab-app
# Simulate disaster: bad env var makes pods crash-loop
sudo k0s kubectl set env deployment/lab-app EXIT_AFTER_BOOT=true
sudo k0s kubectl rollout status deployment/lab-app --timeout=30s || true
sudo k0s kubectl get pods -l app=lab # observe CrashLoopBackOff / unavailable
# Undo
sudo k0s kubectl rollout undo deployment/lab-app
sudo k0s kubectl rollout status deployment/lab-app
sudo k0s kubectl get pods -l app=lab # all healthy again
# Cleanup
sudo k0s kubectl delete deployment lab-app
Key observation moments: the stuck rollout status, the mix of old/new/crashing pods during a halted rollout (old ones keep serving!), and how fast the undo restores service.
๐ฉบ Troubleshooting & Common Issues
Rollout stuck at "X out of Y new replicas have been updated"
sudo k0s kubectl describe deployment <name> | tail -15
sudo k0s kubectl describe pod <one-of-the-new-pods> | grep -A 10 Events
Causes ranked by frequency:
- Readiness probe never passes โ wrong path/port, or app needs longer than configured thresholds. Fix probe config.
- ImagePullBackOff โ typo'd tag or private registry auth missing.
- Insufficient resources โ new pods Pending because
requestsdon't fit any node. Lower requests or free capacity.
"My rollout finished but users saw errors"
Almost always a missing/misconfigured readiness probe โ unready pods entered Service endpoints. Also check terminationGracePeriodSeconds: default 30s may kill long-request handlers mid-flight; raise it and add a graceful-shutdown handler.
CrashLoopBackOff after update
sudo k0s kubectl logs <pod> --previous
The --previous flag shows the crashed attempt's logs. If it's an app-level regression, rollout undo immediately; debug calmly afterward.
Scaling fails: pods stuck Pending on a single-node cluster
Each pod's requests must fit remaining allocatable capacity. Check with:
sudo k0s kubectl describe node <node> | grep -A 8 "Allocated resources"
On small home-lab VMs, keep requests modest (tens of Mi) and use maxSurge percentages instead of fixed counts.
Rollback didn't help
If revision history was pruned (revisionHistoryLimit: 0 โ sometimes set carelessly), there's nothing to roll back to. Keep at least revisionHistoryLimit: 3.
โ Checkpoint
Before moving on, confirm you can:
- Draw the Deployment โ ReplicaSet โ Pod chain and explain where rollbacks come from
- Explain what
maxSurge/maxUnavailableguarantee during an update - Choose the right probe type for "restart me" vs "stop sending me traffic"
- Diagnose a stuck rollout from describe/events in under two minutes
- Execute and verify a rollback to a specific revision
๐ Related
Prerequisites:
- k0s-pods-services - Pod and Service fundamentals
Next in course:
- k0s-configmaps-secrets - Injecting configuration into Deployments
Related topics:
- k0s-storage - Stateful workloads need more than Deployments
- k0s-monitoring - Watching rollout health with metrics
- container-best-practices - Building images that deploy well
๐ Change Log
2026-08-14
- Created lesson covering README spec: Deployments vs pods, replica management, rolling updates, rollbacks, health checks
- Included full release lifecycle hands-on lab
- Added probe configuration guidance and rollout troubleshooting matrix
Next Article: k0s-configmaps-secrets - Configuration and secrets, done right!