Lonjakan HTTP 502 Bad Gateway saat deployment monolitik Inertia.js dengan Server-Side Rendering (SSR) umumnya disebabkan oleh terminasi proses Node.js secara instan tanpa memberi waktu bagi in-flight requests untuk selesai. Ketika eksekusi deployment script mematikan daemon SSR sebelum proses baru siap melayani trafik, socket TCP terputus mendadak dan gateway (Nginx atau runtime PHP) mengembalikan status 502.

Masalah ini dapat dieliminasi secara deterministik dengan menerapkan graceful shutdown berbasis sinyal POSIX, isolasi port upstream, health checking aktif, dan mekanisme safe rollback otomatis pada skrip deployment.

Postmortem: Mengapa HTTP 502 Terjadi Saat Rilis

Arsitektur Inertia SSR standar melibatkan komunikasi internal: Nginx menerima request web, meneruskannya ke PHP-FPM, lalu ekstensi HTTP client Laravel memanggil daemon Node.js lokal (default port 13714) untuk merender HTML awal. Jika SSR crash atau socket tertutup, alur eksekusi menghasilkan error.

Observasi Log dan Metrik APM

Saat deployment berlangsung tanpa koordinasi proses, metrik error rate Nginx dan APM memperlihatkan anomali spesifik:

  • Nginx error.log: Muncul pesan connect() failed (111: Connection refused) while connecting to upstream atau recv() failed (104: Connection reset by peer).
  • APM Tracer: Span HTTP request dari runtime PHP ke endpoint render SSR mengalami status Connection refused dalam rentang 1 hingga 5 detik selama tahapan artisan inertia:stop-ssr atau systemctl restart.
  • Metrik Ketersediaan: Terjadi spike HTTP 502 pada Nginx jika PHP dikonfigurasi melempar HttpException tanpa fallback, atau HTTP 500 langsung ke user akhir.

Akar masalah teknisnya adalah deployment script memicu perintah terminasi paksa (seperti SIGKILL atau kill -9) atau supervisor mematikan worker tanpa menunggu siklus render React/Vue selesai dikompilasi ke string HTML.

Graceful Shutdown dan Connection Draining pada Node.js

Runtime Node.js SSR harus menangkap sinyal SIGTERM dan menolak menerima koneksi baru sembari menuntaskan request yang sedang diproses. Gunakan handler sinyal eksplisit pada file bootstrap SSR (misalnya bootstrap/ssr/ssr.js):

// Inisialisasi HTTP server Inertia SSR
import { createServer } from 'node:http';

const server = createServer((req, res) => {
  // Logika render Inertia
});

const PORT = process.env.PORT || 13714;
server.listen(PORT, '127.0.0.1');

// Tangani sinyal terminasi dari OS/supervisor
let isShuttingDown = false;

function gracefulShutdown(signal) {
  if (isShuttingDown) return;
  isShuttingDown = true;

  console.warn(`[SSR] Menerima sinyal ${signal}. Menutup listener koneksi baru...`);

  // Hentikan penerimaan koneksi TCP baru
  server.close((err) => {
    if (err) {
      console.error('[SSR] Error saat menutup server:', err);
      process.exit(1);
    }
    console.info('[SSR] Seluruh in-flight request selesai. Exit bersih.');
    process.exit(0);
  });

  // Batas toleransi pembersihan (connection draining timeout)
  const FORCE_TIMEOUT_MS = 10000;
  setTimeout(() => {
    console.error('[SSR] Timeout draining terlampaui. Mematikan paksa.');
    process.exit(1);
  }, FORCE_TIMEOUT_MS).unref();
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Konfigurasi Systemd Service Unit

Jika Node.js dikelola menggunakan Systemd, pastikan direktif KillSignal dan timeout penghentian dikonfigurasi agar tidak mengirimkan SIGKILL secara prematur:

[Unit]
Description=Inertia SSR Node.js Daemon
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/app/current
ExecStart=/usr/bin/node bootstrap/ssr/ssr.js
Restart=always
RestartSec=3

# Berikan waktu 15 detik untuk graceful shutdown sebelum SIGKILL dikirim
KillSignal=SIGTERM
KillMode=mixed
TimeoutStopSec=15

# Konfigurasi env
Environment=NODE_ENV=production
Environment=PORT=13714

[Install]
WantedBy=multi-user.target

Konfigurasi Upstream dan Fallback Nginx

Di sisi infrastruktur, Nginx dapat dikonfigurasi agar mentoleransi kegagalan instan upstream melalui mekanisme retry otomatis dan connection pooling.

upstream inertia_ssr_cluster {
    server 127.0.0.1:13714 max_fails=2 fail_timeout=3s;
    # Keepalive pool untuk mengurangi overhead TCP handshake
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;
    root /var/www/app/current/public;

    location @ssr {
        proxy_pass http://inertia_ssr_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;

        # Proteksi kegagalan instan
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        proxy_next_upstream error timeout invalid_header http_502;
        proxy_next_upstream_tries 2;
    }
}

Catatan Fallback: Pada arsitektur monolit Laravel Inertia, jika server Node SSR mati total, method bawaan Inertia akan otomatis fallback merender client-side template (SPA biasa) selama Anda tidak memaksakan throw exception pada konfigurasi config/inertia.php (opsi ssr.throw diatur ke false).

Skrip Deployment Zero-Downtime dan Health Probe Rollback

Pola zero-downtime yang aman memanfaatkan struktur direktori releases dan symlink current. Daemon baru dijalankan pada port sementara atau divalidasi via health check sebelum symlink dialihkan.

#!/usr/bin/env bash
set -euo pipefail

RELEASE_DIR="/var/www/app/releases/$(date +%Y%m%d%H%M%S)"
PREVIOUS_DIR="$(readlink -f /var/www/app/current || true)"
TARGET_PORT=13714

echo "Deploying ke: ${RELEASE_DIR}"
mkdir -p "${RELEASE_DIR}"
git clone --depth 1 [email protected]:org/repo.git "${RELEASE_DIR}"

cd "${RELEASE_DIR}"
composer install --no-dev --optimize-autoloader
npm ci && npm run build

# Verifikasi bundle SSR berhasil dikompilasi
if [ ! -f "bootstrap/ssr/ssr.js" ]; then
    echo "[ERROR] Bundle SSR tidak ditemukan. Batalkan deployment!"
    rm -rf "${RELEASE_DIR}"
    exit 1
fi

# Switch symlink ke rilis baru
ln -sfn "${RELEASE_DIR}" /var/www/app/current_candidate

# Reload systemd service (memicu SIGTERM ke worker lama via graceful reload)
sudo systemctl reload-or-restart inertia-ssr.service

# Health check probe loop
MAX_RETRIES=5
RETRY_COUNT=0
HEALTHY=false

echo "Menjalankan health probe SSR..."
until [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; do
    if curl -s -f -o /dev/null "http://127.0.0.1:${TARGET_PORT}/healthz"; then
        HEALTHY=true
        break
    fi
    RETRY_COUNT=$((RETRY_COUNT + 1))
    echo "Probe gagal (${RETRY_COUNT}/${MAX_RETRIES}). Coba lagi dalam 1s..."
    sleep 1
done

if [ "$HEALTHY" = true ]; then
    # Promosi rilis candidate ke active
    mv -Tf /var/www/app/current_candidate /var/www/app/current
    echo "[SUKSES] Rilis aktif berhasil dialihkan."
else
    echo "[CRITICAL] SSR Service gagal merespons. Menjalankan Rollback!"
    rm -f /var/www/app/current_candidate
    if [ -n "${PREVIOUS_DIR}" ]; then
        ln -sfn "${PREVIOUS_DIR}" /var/www/app/current
        sudo systemctl restart inertia-ssr.service
        echo "Rollback selesai ke: ${PREVIOUS_DIR}"
    fi
    exit 1
fi

Pencegahan Regresi pada Pipeline CI/CD

Untuk mencegah kode yang merusak siklus hidup SSR lolos ke server produksi, tambahkan validasi otomatis pada tahap pengujian CI:

  1. Dry-run SSR Initialization: Jalankan node bootstrap/ssr/ssr.js --check-only atau jalankan proses di background lalu eksekusi single-render test menggunakan curl sebelum build artifact dipaketkan.
  2. Linting Memory Leaks: Pastikan tidak ada global state yang di-share antar-request di komponen Vue/React SSR. Global state yang tidak dibersihkan menyebabkan leak memori, memicu OOM (Out Of Memory) killer mematikan Node secara mendadak dengan sinyal SIGKILL.
  3. Timeout Monitoring: Pasang alert APM khusus untuk HTTP request antar PHP dan daemon Node.js. Jika latensi SSR internal melebihi 200ms, tim teknis harus segera menerima notifikasi sebelum kapasitas pool habis.