DevOps24
← Back to Blog
How to Set Up Zero-Downtime Kubernetes Deployments

How to Set Up Zero-Downtime Kubernetes Deployments

Step-by-step guide to zero-downtime deployments on Kubernetes. Rolling updates, readiness probes, PodDisruptionBudgets and real-world lessons from a Black Friday migration.

Every deployment is a risk. The question is how much risk you’re willing to accept — and whether your users ever need to notice.


Zero-downtime deployments on Kubernetes are achievable with the right configuration. They’re also easy to get wrong in ways that only surface under production load. This guide covers what actually works, based on running migrations for e-commerce platforms, SaaS products and fintech applications where downtime has a direct revenue cost.


Why deployments cause downtime in the first place


Most Kubernetes deployments cause downtime for one of three reasons.


The new pod starts receiving traffic before it’s ready. Kubernetes marks a pod as running the moment the container starts — but your application might need 10, 20 or 30 seconds to initialise before it can handle requests. Without a readiness probe, Kubernetes sends traffic to a pod that isn’t ready yet.


The old pod is terminated before the new one is fully serving traffic. If your rolling update replaces pods faster than new ones become ready, there’s a window where neither the old nor the new pod is handling requests cleanly.


The application doesn’t handle SIGTERM gracefully. When Kubernetes terminates a pod, it sends SIGTERM and waits for the process to exit. If your application exits immediately without finishing in-flight requests, those requests fail.


The foundation: readiness and liveness probes


Readiness probes are the single most important configuration for zero-downtime deployments. Without them, nothing else matters.


yaml

readinessProbe:

httpGet:

path: /health/ready

port: 3000

initialDelaySeconds: 10

periodSeconds: 5

failureThreshold: 3

successThreshold: 1

livenessProbe:

httpGet:

path: /health/live

port: 3000

initialDelaySeconds: 30

periodSeconds: 10

failureThreshold: 3


The readiness probe tells Kubernetes when the pod is ready to receive traffic. The liveness probe tells Kubernetes when the pod has entered a broken state and should be restarted.


These are different things. A pod can be live (the process is running) but not ready (still loading configuration, warming up caches, waiting for a database connection). Your application should expose separate endpoints for each.


A minimal health endpoint in Node.js:


javascript

// Readiness — app is ready to handle requests

app.get('/health/ready', (req, res) => {

if (db.isConnected() && cache.isReady()) {

res.status(200).json({ status: 'ready' });

} else {

res.status(503).json({ status: 'not ready' });

}

});


// Liveness — app is alive (not deadlocked)

app.get('/health/live', (req, res) => {

res.status(200).json({ status: 'alive' });

});


Rolling update strategy


Kubernetes uses a RollingUpdate strategy by default, but the default configuration is too aggressive for production workloads:


yaml

strategy:

type: RollingUpdate

rollingUpdate:

maxSurge: 1

maxUnavailable: 0


maxUnavailable: 0 means Kubernetes will never terminate an old pod until a new pod is fully ready. This is the key setting. With the default maxUnavailable: 1, Kubernetes can terminate an old pod immediately, leaving you briefly under capacity.


maxSurge: 1 allows one extra pod during the rollout. If you run 3 replicas, during deployment you’ll temporarily have 4. Make sure your cluster has capacity for this.


Graceful shutdown


Your application must handle SIGTERM and finish in-flight requests before exiting:


javascript

process.on('SIGTERM', () => {

console.log('SIGTERM received — shutting down gracefully');

server.close(() => {

console.log('All connections closed');

process.exit(0);

});


// Force exit after 30 seconds if connections haven't closed

setTimeout(() => {

console.error('Forcing exit after timeout');

process.exit(1);

}, 30000);

});


Set terminationGracePeriodSeconds in your pod spec to give the application enough time to drain connections:


yaml

spec:

terminationGracePeriodSeconds: 60

containers:

- name: app

...


The sequence when Kubernetes terminates a pod: SIGTERM is sent → pod is removed from the service endpoints (no new traffic) → application finishes in-flight requests → process exits. The key detail: pod removal from endpoints and SIGTERM happen simultaneously. There’s a propagation delay of a few seconds before all nodes stop routing traffic to the pod. Add a preStop sleep to account for this:


yaml

lifecycle:

preStop:

exec:

command: ["/bin/sh", "-c", "sleep 5"]


This 5-second sleep gives the endpoint removal time to propagate before your application starts refusing connections.


PodDisruptionBudgets


Rolling updates aren’t the only thing that terminates pods. Node maintenance, cluster upgrades and autoscaling can all evict pods simultaneously. A PodDisruptionBudget sets a floor on availability:


yaml

apiVersion: policy/v1

kind: PodDisruptionBudget

metadata:

name: app-pdb

spec:

minAvailable: 2

selector:

matchLabels:

app: your-app


With minAvailable: 2 and 3 replicas, Kubernetes will never evict more than 1 pod at a time — even during node maintenance. Without this, a node drain can terminate all pods on that node simultaneously.


Deployment verification with kubectl rollout


After deploying, watch the rollout in real time:


bash

kubectl rollout status deployment/your-app --timeout=5m


This command blocks until the rollout completes or times out. Use it in your CI/CD pipeline to catch failed deployments before they affect users:


yaml

# In your CI/CD pipeline

- kubectl apply -f k8s/deployment.yaml

- kubectl rollout status deployment/your-app --timeout=5m || kubectl rollout undo deployment/your-app


The || kubectl rollout undo automatically rolls back if the deployment doesn’t complete within 5 minutes — for example, if new pods fail their readiness probe.


A real-world example


An e-commerce client came to me before Black Friday with a deployment process that caused 5 minutes of downtime every release. Their development team had stopped deploying on Fridays because they were afraid of breaking the store before the weekend.


The issues: no readiness probes (traffic hit pods before the app was ready), maxUnavailable: 1 (pods were terminated before replacements were healthy), no graceful shutdown (in-flight checkout requests were dropped on pod termination).


After implementing the configuration above, their next deployment was invisible to users. Deploy time dropped from “avoid it on Fridays” to a 90-second rollout that runs in CI/CD on every merge to main. That Black Friday — their highest traffic day — was the first one without an incident.


Summary configuration


yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: your-app

spec:

replicas: 3

strategy:

type: RollingUpdate

rollingUpdate:

maxSurge: 1

maxUnavailable: 0

template:

spec:

terminationGracePeriodSeconds: 60

containers:

- name: app

image: your-app:latest

lifecycle:

preStop:

exec:

command: ["/bin/sh", "-c", "sleep 5"]

readinessProbe:

httpGet:

path: /health/ready

port: 3000

initialDelaySeconds: 10

periodSeconds: 5

failureThreshold: 3

livenessProbe:

httpGet:

path: /health/live

port: 3000

initialDelaySeconds: 30

periodSeconds: 10

failureThreshold: 3


Zero-downtime deployments aren’t a Kubernetes feature you enable — they’re a combination of correct probe configuration, a conservative rolling update strategy and an application that handles shutdown gracefully. Each piece matters. Miss one and you’ll see failures under load.


If your team is still afraid of deployments, or if you’re planning a migration to Kubernetes and want to get this right from the start — that’s exactly the kind of work I do. Fixed scope, defined timeline, measurable outcome.


Get in touch → devops24.eu/#contact