In high-concurrency production platforms, restarting application services without degrading uptime (Zero-Downtime Deployment) is a foundational Site Reliability Engineering (SRE) mandate. Deploying a new release of a Server-Side Rendered (SSR) Next.js 15 application in container orchestrators (Kubernetes, Docker Swarm) carries inherent risks of terminating active client TCP connections, emitting HTTP 502 Bad Gateway errors, and corrupting in-flight state.
When an orchestrator signals a container to stop, the operating system dispatches a SIGTERM signal. If the application terminates immediately, all in-flight HTTP requests are abruptly severed. In distributed cluster networks, this disruption is amplified by the asynchronous propagation delay of routing table updates:
Terminating and concurrently triggers removal of its IP from the associated EndpointSlice.kube-proxy daemon on every cluster node introduces a 1–3 second latency while reprogramming host iptables or IPVS packet-filtering rules.502 Bad Gateway or ECONNREFUSED errors.To eliminate release-induced degradation, delivery architecture relies on three primary deployment strategies:
maxSurge, maxUnavailable) and synchronizing readiness via readinessProbe and preStop hooks.The lifecycle of network traffic during generational Pod rotation and weighted Canary routing is illustrated below:
[ Inbound Client HTTP Traffic ]
│
▼
[ Ingress Controller (Nginx / Envoy) ]
│ │
(90% Traffic) (10% Traffic)
│ │
▼ ▼
[ Service: production-v1 ] [ Service: production-v2-canary ]
│ │
┌─────────┴─────────┐ │
▼ ▼ ▼
[ Pod v1 ] [ Pod v1 ] [ Pod v2 ]
(Active) (Active) (Canary Validation)
│ │ │
└───────┬───────────┘ │
▼ ▼
[ Database: Schema Contract v1/v2 (Backward Compatible) ]maxUnavailable: 0 alongside pre-warmed SSR cache layers.X-Feature-Group cookies to evaluate an updated checkout engine against 5% of users before initiating global rollout.Achieving true zero-downtime delivery for Next.js 15 requires harmonizing three distinct layers: application-level SIGTERM interception in Node.js, Kubernetes Pod lifecycle configuration, and L7 Ingress weighted traffic steering.
In output: 'standalone' mode, Next.js generates a compact Node.js HTTP server. The runtime must catch OS termination signals, reject incoming TCP handshakes, and allow active in-flight requests to complete within a deterministic deadline:
import http from 'node:http';
import next from 'next';
const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';
const port = parseInt(process.env.PORT || '3000', 10);
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = http.createServer((req, res) => {
handle(req, res);
});
// Track open TCP connections for forceful destruction if deadline expires
const connections = new Set<import('node:net').Socket>();
server.on('connection', (socket) => {
connections.add(socket);
socket.once('close', () => connections.delete(socket));
});
let isShuttingDown = false;
// Liveness & Readiness healthcheck endpoint
server.on('request', (req, res) => {
if (req.url === '/api/healthz') {
if (isShuttingDown) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'terminating' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
return;
}
});
server.listen(port, () => {
console.log(`[Next.js Runtime] Server ready on http://${hostname}:${port}`);
});
async function handleGracefulShutdown(signal: string) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`[Next.js Runtime] Received ${signal}. Starting graceful shutdown sequence...`);
// 1. Stop accepting new incoming HTTP connections
server.close((err) => {
if (err) {
console.error('[Next.js Runtime] Error during HTTP server close:', err);
process.exit(1);
}
console.log('[Next.js Runtime] Closed all active HTTP connections successfully.');
process.exit(0);
});
// 2. Enforce strict timeout deadline for draining in-flight requests (10 seconds)
setTimeout(() => {
console.warn('[Next.js Runtime] Forcing shutdown: active connections exceeded timeout limit.');
for (const socket of connections) {
socket.destroy();
}
process.exit(1);
}, 10000).unref();
}
process.on('SIGTERM', () => handleGracefulShutdown('SIGTERM'));
process.on('SIGINT', () => handleGracefulShutdown('SIGINT'));
});Compile container image pinned to git commit SHA, execute security scans, and push to container registry.
Provision isolated v2 deployment with minimal replicas. Satisfy startupProbe and readinessProbe before traffic ingestion.
Ingress controller splits incoming traffic, directing 10% of requests to the Canary Service via weighted round-robin.
Monitor p99 latency quantiles and HTTP 5xx error ratios. If telemetry remains within thresholds, scale traffic towards 100%.
Promote stable deployment to v2 image. Deprovision canary Ingress. Decommission old pods gracefully via preStop hooks.
Below is an enterprise-grade Kubernetes manifest configuring a stable baseline deployment with zero-unavailable rolling updates, preStop delays, deep health probes, and a weighted canary Ingress.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-web-stable
namespace: production
labels:
app: nextjs-web
track: stable
spec:
replicas: 4
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
# Prevent active pods from falling below 100% of replicas
maxUnavailable: 0
# Provision at most 1 additional pod during rolling rotation
maxSurge: 1
selector:
matchLabels:
app: nextjs-web
track: stable
template:
metadata:
labels:
app: nextjs-web
track: stable
spec:
terminationGracePeriodSeconds: 30
containers:
- name: nextjs-app
image: ghcr.io/enterprise/nextjs-web:v1.24.0
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
# Sleep delay ensures kube-proxy and Ingress evict IP before SIGTERM
command: ["/bin/sh", "-c", "sleep 15"]
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3000"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1536Mi"
# Protects container from premature termination during initial build hydration
startupProbe:
httpGet:
path: /api/healthz
port: 3000
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 20
# Add to balancing pool only when fully ready to handle client requests
readinessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
# Restart hung event loops or deadlocked processes
livenessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 10
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: nextjs-service-stable
namespace: production
spec:
type: ClusterIP
selector:
app: nextjs-web
track: stable
ports:
- name: http
port: 80
targetPort: 3000
---
# Canary Deployment: Deploy v1.25.0 to handle 10% of traffic
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-web-canary
namespace: production
labels:
app: nextjs-web
track: canary
spec:
replicas: 2
selector:
matchLabels:
app: nextjs-web
track: canary
template:
metadata:
labels:
app: nextjs-web
track: canary
spec:
terminationGracePeriodSeconds: 30
containers:
- name: nextjs-app
image: ghcr.io/enterprise/nextjs-web:v1.25.0
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
ports:
- containerPort: 3000
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1536Mi"
startupProbe:
httpGet:
path: /api/healthz
port: 3000
failureThreshold: 20
periodSeconds: 3
readinessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: nextjs-service-canary
namespace: production
spec:
type: ClusterIP
selector:
app: nextjs-web
track: canary
ports:
- name: http
port: 80
targetPort: 3000
---
# Primary Baseline Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextjs-ingress-stable
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- host: app.enterprise.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextjs-service-stable
port:
number: 80
---
# Canary Ingress: Divert 10% weighted traffic to v1.25.0
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextjs-ingress-canary
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
# Header override for internal QA testing
nginx.ingress.kubernetes.io/canary-by-header: "X-Canary-Internal"
spec:
rules:
- host: app.enterprise.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextjs-service-canary
port:
number: 80By default, Kubernetes dispatches SIGTERM to the container and begins evicting the IP from the EndpointSlice simultaneously. Because updating kernel packet tables (iptables/IPVS) across worker nodes incurs a 1–3 second propagation delay, ingress controllers continue routing new requests to the terminating Pod, triggering a flood of HTTP 502 Bad Gateway errors. Always declare preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] in container specifications.
Setting maxUnavailable: 1 or maxUnavailable: 25% in a 2-pod deployment drops total cluster serving capacity by 50% during updates. Under peak traffic, the single remaining instance suffers load-shedding or crashes via OOMKilled, causing catastrophic service failure. Mission-critical architectures must specify maxUnavailable: 0 and rely on maxSurge.
Altering or dropping a PostgreSQL column before deploying new application code immediately breaks the active baseline version (v1), which continues executing queries against the removed column. In Blue-Green and Canary architectures, database migrations must precede application releases: introduce new fields (Expand), deploy software supporting both contracts, and remove deprecated columns only after older code versions have been fully decommissioned (Contract).
When Next.js performs on-demand route compilation or warms memory caches during boot (taking 15–20 seconds), standard aggressive livenessProbe checks (e.g., failure threshold of 3 with a 5-second interval) kill the container before it finishes initializing, locking the workload in a perpetual CrashLoopBackOff state. Allocate dedicated initialization headroom using startupProbe.
Apply canary-deployment manifest with the candidate image; ensure pods pass startupProbe initialization.
Successful readinessProbe responses attach canary pod IP addresses to the isolated canary Service.
Apply canary-weight annotations to route one-tenth of inbound production traffic to the candidate service.
Poll Prometheus metrics for HTTP 5xx spikes, request timeouts, and p99 latency degradation.
Upgrade the stable deployment to the candidate image, decommission the canary Ingress, and drain old pods gracefully.