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:


๐Ÿ—๏ธ 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:

  1. Liveness checks must be cheap and internal โ€” no database calls. A DB blip would otherwise restart every pod simultaneously (self-inflicted outage).
  2. Readiness CAN check dependencies โ€” being removed from endpoints while the DB is down is correct behavior.
  3. 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.
  4. 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:

  1. Readiness probe never passes โ€” wrong path/port, or app needs longer than configured thresholds. Fix probe config.
  2. ImagePullBackOff โ€” typo'd tag or private registry auth missing.
  3. Insufficient resources โ€” new pods Pending because requests don'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:


๐Ÿ”— Related

Prerequisites:

Next in course:

Related topics:


๐Ÿ“ Change Log

2026-08-14


Next Article: k0s-configmaps-secrets - Configuration and secrets, done right!

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse