En plataformas de producción con alta concurrencia, reiniciar los servicios de la aplicación sin degradar la disponibilidad (Zero-Downtime Deployment) constituye un requisito fundamental de la ingeniería de confiabilidad de sitios (SRE). Desplegar una nueva versión de una aplicación SSR en Next.js 15 dentro de orquestadores de contenedores (Kubernetes, Docker Swarm) conlleva riesgos inherentes de corte abrupto de conexiones TCP activas de clientes, generación de errores HTTP 502 Bad Gateway y corrupción de estados transaccionales en tránsito.
Cuando el orquestador solicita la detención de un contenedor, el sistema operativo despacha una señal SIGTERM. Si la aplicación finaliza de forma inmediata, todas las peticiones HTTP activas en curso (in-flight requests) se interrumpen de manera abrupta. En redes de clústeres distribuidos, esta interrupción se agrava debido a la latencia de propagación asíncrona de las tablas de enrutamiento:
Terminating y de forma concurrente inicia la eliminación de su dirección IP del recurso EndpointSlice.kube-proxy en cada nodo del clúster introduce una latencia de 1 a 3 segundos mientras reprograma las reglas de filtrado de paquetes del kernel mediante iptables o IPVS.502 Bad Gateway o ECONNREFUSED.Para suprimir por completo los fallos durante los lanzamientos, la arquitectura de entrega se apoya en tres estrategias cardinales de despliegue:
maxSurge, maxUnavailable) y sincronizando la disponibilidad mediante readinessProbe y hooks preStop.El ciclo de vida del tráfico de red durante la rotación generacional de Pods y el enrutamiento ponderado Canary se ilustra a continuación:
[ Tráfico HTTP Entrante de Clientes ]
│
▼
[ Ingress Controller (Nginx / Envoy) ]
│ │
(90% Tráfico) (10% Tráfico)
│ │
▼ ▼
[ Service: production-v1 ] [ Service: production-v2-canary ]
│ │
┌─────────┴─────────┐ │
▼ ▼ ▼
[ Pod v1 ] [ Pod v1 ] [ Pod v2 ]
(Activo) (Activo) (Validación Canary)
│ │ │
└───────┬───────────┘ │
▼ ▼
[ Base de Datos: Esquema Expand-Contract (Compatible hacia atrás) ]maxUnavailable: 0 y precalentamiento de memorias intermedias SSR.X-Feature-Group para someter a prueba un nuevo motor de checkout con el 5% de los usuarios antes de iniciar el lanzamiento global.Lograr una entrega continua sin tiempo de inactividad para Next.js 15 exige coordinar tres niveles: la interceptación programática de SIGTERM en el runtime de Node.js, la configuración declarativa del ciclo de vida de los Pods en los manifiestos de Kubernetes y el enrutamiento ponderado en el Ingress L7.
En el modo output: 'standalone', Next.js genera un servidor HTTP de Node.js autocontenido. El runtime debe capturar las señales de terminación del sistema operativo, rechazar nuevas negociaciones TCP y procesar todas las solicitudes activas pendientes dentro de un plazo límite determinista:
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);
});
// Seguimiento de conexiones TCP abiertas para cierre forzado si expira el plazo
const connections = new Set<import('node:net').Socket>();
server.on('connection', (socket) => {
connections.add(socket);
socket.once('close', () => connections.delete(socket));
});
let isShuttingDown = false;
// Endpoint de comprobación de salud Liveness y Readiness
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] Servidor listo en http://${hostname}:${port}`);
});
async function handleGracefulShutdown(signal: string) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`[Next.js Runtime] Señal ${signal} recibida. Iniciando secuencia de apagado controlado...`);
// 1. Detener la admisión de nuevas conexiones HTTP entrantes
server.close((err) => {
if (err) {
console.error('[Next.js Runtime] Error al cerrar el servidor HTTP:', err);
process.exit(1);
}
console.log('[Next.js Runtime] Se cerraron todas las conexiones HTTP activas con éxito.');
process.exit(0);
});
// 2. Establecer un tiempo límite estricto para vaciar peticiones en vuelo (10 segundos)
setTimeout(() => {
console.warn('[Next.js Runtime] Apagado forzado: las conexiones activas excedieron el tiempo límite.');
for (const socket of connections) {
socket.destroy();
}
process.exit(1);
}, 10000).unref();
}
process.on('SIGTERM', () => handleGracefulShutdown('SIGTERM'));
process.on('SIGINT', () => handleGracefulShutdown('SIGINT'));
});Generación de imagen inmutable vinculada al SHA del commit, ejecución de análisis de vulnerabilidades y publicación en el registro.
Aprovisionamiento de un deployment v2 aislado con mínimas réplicas. Superación exitosa de startupProbe y readinessProbe.
El controlador de Ingress desvía el 10% de las peticiones hacia el Canary Service utilizando round-robin ponderado.
Monitorización de latencias p99 y tasas de error HTTP 5xx. Si las métricas se mantienen estables, se eleva el tráfico al 100%.
Actualización del deployment estable a la versión v2. Eliminación de Canary Ingress y detención gradual de pods antiguos mediante preStop.
A continuación se presenta un manifiesto de Kubernetes para entornos de producción que implementa actualizaciones continuas seguras, retardos preStop, sondas de comprobación avanzadas e Ingress ponderado para releases Canary.
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:
# Prohíbe que el número de pods activos disminuya por debajo del 100% de replicas
maxUnavailable: 0
# Aprovisiona como máximo 1 pod adicional durante la rotación
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:
# El retraso permite a kube-proxy e Ingress desalojar la IP antes del 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"
# Protege al contenedor de cierres prematuros durante la compilación/hidratación inicial
startupProbe:
httpGet:
path: /api/healthz
port: 3000
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 20
# Incorpora el pod al balanceador solo cuando está completamente listo para procesar tráfico
readinessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
# Reinicia procesos bloqueados o bucles de eventos congelados
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: despliegue de v1.25.0 para atender el 10% del tráfico
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
---
# Ingress Estable Principal
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
---
# Ingress Canary: desvío del 10% ponderado del tráfico a 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"
# Cabecera para validación interna por el equipo de QA
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: 80Por defecto, Kubernetes envía la señal SIGTERM al contenedor y al mismo tiempo inicia la remoción de la IP del recurso EndpointSlice. Debido a la latencia de actualización de reglas del kernel (iptables/IPVS) en los nodos trabajadores (1 a 3 segundos), los balanceadores Ingress continúan despachando peticiones al Pod en proceso de finalización, provocando una oleada de errores HTTP 502 Bad Gateway. Declare siempre preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] en la configuración del contenedor.
Fijar maxUnavailable: 1 o maxUnavailable: 25% en un despliegue de 2 réplicas reduce la capacidad operativa del clúster en un 50% durante las rotaciones. En momentos de alta carga, la única instancia activa restante se sobrecarga o falla por agotamiento de memoria (OOMKilled), ocasionando una caída general del servicio. En sistemas de misión crítica, defina maxUnavailable: 0 y utilice maxSurge.
Modificar o eliminar una columna en PostgreSQL antes de desplegar la nueva versión del código romperá de inmediato la versión estable activa (v1), la cual continúa ejecutando consultas sobre la columna retirada. En arquitecturas Blue-Green y Canary, la base de datos debe adelantarse al código: incorporar primero los nuevos campos (Expand), desplegar software compatible con ambos esquemas y solo retirar las columnas obsoletas tras dar de baja las versiones anteriores (Contract).
Si Next.js compila paquetes de rutas o precalienta cachés en memoria durante 15 a 20 segundos tras iniciar, una comprobación agresiva de livenessProbe (como 3 fallos con intervalo de 5 segundos) terminará el contenedor antes de completar su inicio. Esto sitúa la carga en un estado cíclico de CrashLoopBackOff. Proporcione el margen de tiempo necesario mediante startupProbe.
Aplicar el manifiesto canary-deployment con la nueva imagen; asegurar que los pods superan la verificación de startupProbe.
Las respuestas satisfactorias de readinessProbe conectan las direcciones IP de los pods canarios al Service dedicado.
Aplicar anotaciones canary-weight para dirigir la décima parte del tráfico entrante al servicio canario.
Consultar métricas de Prometheus para detectar incrementos de errores 5xx, tiempos de espera y latencias p99.
Actualizar la versión de la imagen en el despliegue estable, retirar el Ingress canario y apagar pods antiguos de forma controlada.