Kubernetes Deployments

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

How Deployments manage ReplicaSets and pods, how rolling updates and rollbacks actually work, and how to debug a rollout that never finishes.

A Deployment is how you run a stateless application on Kubernetes. You declare what you want, this image, this many replicas, and a controller works continuously to make reality match, replacing failed pods, rolling out new versions gradually, and keeping enough history to undo a bad release.

It is the resource you will use for the overwhelming majority of workloads.

Why not manage pods yourself?

A bare pod has no controller watching it. When its node reboots, the pod is gone and stays gone. Nothing scales it, nothing replaces it, and updating it means deleting and recreating it by hand with an outage in between.

A Deployment gives you self-healing, scaling, gradual rollouts, and a documented way back.

The chain: Deployment → ReplicaSet → Pods

This indirection confuses people until they see why it exists.

A Deployment does not create pods. It creates a ReplicaSet, and the ReplicaSet creates pods. Each time you change the pod template, a new image, a changed environment variable, the Deployment creates a new ReplicaSet and gradually scales the old one down while scaling the new one up.

Deployment → ReplicaSet → Pods

Deployment

my-app

replicas: 3 · strategy: RollingUpdate

manages

ReplicaSet

my-app-7d9f4

desired: 3 · ready: 3

owns

Pod

Pod 1

Pod

Pod 2

Pod

Pod 3

kubectl get replicasets
NAME                      DESIRED   CURRENT   READY   AGE
payments-api-7d4f9c8b5    3         3         3       2m
payments-api-5c9b7d446    0         0         0       1h

The old ReplicaSet is kept at zero replicas deliberately. That retained object is your rollback. Undoing a release scales it back up. revisionHistoryLimit controls how many are kept, defaulting to 10.

One consequence worth knowing: scaling does not create a revision. Only pod template changes do. Changing replicas from 3 to 5 adjusts the current ReplicaSet in place.

Writing a Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: payments-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: payments-api
    spec:
      containers:
        - name: api
          image: ghcr.io/acme/payments-api:1.4.2
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10

selector.matchLabels must match template.metadata.labels. The API server rejects a Deployment where they disagree, which is a good thing. The selector is how the Deployment finds the pods it owns.

The selector is also immutable after creation. If you need to change it, you delete and recreate the Deployment. Choose labels you can live with.

A readinessProbe is what makes a rolling update safe. Without one, Kubernetes considers a pod ready the instant its container process starts, so it will send traffic to an application still loading configuration or warming a connection pool, and, worse, will proceed to terminate the next old pod. The rollout looks clean and users see errors.

Rolling updates

# Update the image
kubectl set image deployment/payments-api api=ghcr.io/acme/payments-api:1.5.0

# Watch it happen
kubectl rollout status deployment/payments-api
Waiting for deployment "payments-api" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "payments-api" rollout to finish: 2 of 3 updated replicas are available...
deployment "payments-api" successfully rolled out

Two settings control the shape of the rollout:

SettingMeaningEffect
maxSurgeExtra pods allowed above replicasHigher = faster, needs spare capacity
maxUnavailablePods allowed to be down during the update0 = no capacity dip, but requires surge room

maxSurge: 1, maxUnavailable: 0 is the safe default: never fewer healthy pods than you asked for, at the cost of one extra pod's resources during the rollout. The opposite, maxUnavailable: 1, maxSurge: 0, updates in place with no extra capacity but runs degraded, which only makes sense on a tightly constrained cluster.

The alternative strategy is Recreate: terminate everything, then start the new version. It causes downtime by design and exists for applications that genuinely cannot run two versions at once, such as one holding an exclusive lock or applying incompatible schema migrations.

Rollbacks

# What has happened to this Deployment?
kubectl rollout history deployment/payments-api
REVISION  CHANGE-CAUSE
3         Deploy 1.5.0, new pricing engine
2         Deploy 1.4.2
1         Initial release
# Undo the last change
kubectl rollout undo deployment/payments-api

# Or go to a specific revision
kubectl rollout undo deployment/payments-api --to-revision=2

That CHANGE-CAUSE column comes from a kubernetes.io/change-cause annotation, and it is empty unless you set it. Do set it. During an incident, a history of revision numbers with no descriptions is nearly useless:

kubectl annotate deployment/payments-api \
  kubernetes.io/change-cause="Deploy 1.5.0, new pricing engine" --overwrite

If a rollout is going badly and you want it to stop without reverting, pause it:

kubectl rollout pause deployment/payments-api
kubectl rollout resume deployment/payments-api

Pausing is also how you batch several changes into one rollout instead of triggering a separate one per edit.

When a rollout will not finish

This is the most common Deployment problem, and the diagnosis is nearly always the same shape.

kubectl rollout status deployment/payments-api
Waiting for deployment "payments-api" rollout to finish: 1 out of 3 new replicas have been updated...
error: deployment "payments-api" exceeded its progress deadline

progressDeadlineSeconds defaults to 600, so a stuck rollout reports failure after ten minutes. Critically, it does not roll back. It simply stops and tells you. The old pods keep serving, which is why a failed deploy usually is not an outage.

Look at the new pods, not the Deployment:

kubectl get pods -l app=payments-api
kubectl describe pod <the-new-pending-or-crashing-one>

The realistic causes:

  • New pods never become Ready: the readiness probe is failing, wrong path, wrong port, or too short an initialDelaySeconds for a slow-starting application.
  • ImagePullBackOff: the tag does not exist or the registry credentials are missing.
  • Pending: with maxUnavailable: 0 the cluster must fit one extra pod, and there is no room.
  • CrashLoopBackOff: the new version fails at startup; kubectl logs <pod> --previous has the reason.

A rollout blocked because new pods will not schedule is the reason maxSurge/maxUnavailable matters on a full cluster.

Deployment or StatefulSet?

DeploymentStatefulSet
Pod namesRandom suffixStable ordinals: db-0, db-1
StorageShared or noneA PersistentVolumeClaim per pod
Start/stop orderAll at onceOrdered, one at a time
Network identityNoneStable DNS per pod
Use forWeb apps, APIs, workersDatabases, queues, anything clustered

If your application is happy having any replica handle any request and keeps no local state, use a Deployment. Databases and quorum-based systems need the stable identity a StatefulSet provides.

Explore further in this section

Sub-topicWhat you will learn
Create DeploymentsWrite YAML, apply it, scale it, inspect ReplicaSets
Rolling UpdatesRollout strategy and image updates in depth
RollbacksRevert safely and read rollout history

Quick reference

TaskCommand
Create from YAMLkubectl apply -f deployment.yaml
Update the imagekubectl set image deployment/<n> <container>=<image>
Watch a rolloutkubectl rollout status deployment/<n>
Revision historykubectl rollout history deployment/<n>
Roll backkubectl rollout undo deployment/<n> [--to-revision=N]
Pause / resumekubectl rollout pause|resume deployment/<n>
Scalekubectl scale deployment/<n> --replicas=5
Force a restartkubectl rollout restart deployment/<n>
See the ReplicaSetskubectl get rs -l app=<label>

Key takeaways

  • Deployment → ReplicaSet → Pods. Each template change makes a new ReplicaSet; the old one at zero replicas is your rollback.
  • Scaling does not create a revision: only pod template changes do.
  • The selector is immutable. Changing it means deleting and recreating the Deployment.
  • A readiness probe is what makes a rolling update safe. Without one Kubernetes shifts traffic to pods that are not ready yet.
  • maxSurge: 1, maxUnavailable: 0 keeps full capacity during a rollout, provided the cluster has room for one extra pod.
  • A failed rollout stops, it does not auto-revert. Old pods keep serving, so diagnose the new pods before doing anything drastic.
  • Set kubernetes.io/change-cause, or your rollout history is a list of numbers during an incident.

Check your understanding

Exercise

A clean rollout with dirty results

A Deployment with maxUnavailable: 0 has no readiness probe. During every rollout users see brief 502 errors, yet kubectl rollout status reports success. Why?

Exercise

A rollout that gave up

kubectl rollout status returns "exceeded its progress deadline" after ten minutes. What is the state of the application right now?


Frequently Asked Questions

What is the difference between a Deployment and a ReplicaSet?

A ReplicaSet keeps a specified number of identical pods running, that is all it does. A Deployment manages ReplicaSets and adds everything around versioning: when you change the pod template it creates a new ReplicaSet, shifts replicas from old to new gradually, and keeps the previous ReplicaSets so you can roll back. You almost never create a ReplicaSet directly; you create a Deployment and it manages them for you.

Why is my deployment stuck and not rolling out?

The rollout is waiting for new pods to become Ready and they never do. Ignore the Deployment and look at the pods: kubectl get pods -l app=<label> then kubectl describe pod <new-pod>. The usual causes are a readiness probe pointed at the wrong path or port, an initialDelaySeconds shorter than the application's real startup time, an image tag that does not exist, a crash at startup visible via kubectl logs <pod> --previous, or, with maxUnavailable: 0, no room in the cluster for the one extra surge pod.

How do I roll back a bad deployment?

kubectl rollout undo deployment/<name> reverts to the previous revision, and --to-revision=N targets a specific one from kubectl rollout history. This works because old ReplicaSets are retained at zero replicas, so a rollback is really just scaling one of them back up. It is fast, but note that revisionHistoryLimit (default 10) bounds how far back you can go, and a rollback does not undo anything outside Kubernetes. A database migration applied by the bad release still needs handling separately.

What do maxSurge and maxUnavailable actually do?

They bound the rollout. maxSurge is how many pods above your replica count may exist temporarily, and maxUnavailable is how many of your desired pods may be down at once. maxSurge: 1, maxUnavailable: 0 never dips below full capacity but needs room for one extra pod. maxSurge: 0, maxUnavailable: 1 uses no extra capacity but runs one pod short during the update. Both accept percentages, which is more useful at larger replica counts.

How do I restart all pods in a deployment without changing the image?

kubectl rollout restart deployment/<name>. It sets a timestamp annotation on the pod template, which counts as a template change and triggers a normal rolling update, so it respects your surge and unavailability settings and causes no downtime. This is the correct way to pick up a changed ConfigMap or Secret mounted as environment variables, since those do not restart pods on their own.

When should I use a StatefulSet instead?

When pod identity matters. StatefulSets give each pod a stable name and DNS entry that survives rescheduling, create a PersistentVolumeClaim per pod, and start and stop pods in order. That is what databases, message brokers and quorum-based systems need. A replica has to come back as the same member with the same data. If any replica can serve any request and keeps nothing on local disk, a Deployment is simpler and is the right choice.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources