karawaci.kode

2026-08-21 · 10 min

Zero-Downtime Deploy Blue-Green di VPS Single Core dengan Nginx dan systemd

Klien saya di Jakarta punya VPS Hetzner CAX11 single core dengan dua service Node.js yang sebelumnya di-deploy pakai PM2 restart. Setiap kali ada deploy, ada window 5-10 detik di mana aplikasi sedang restart dan user mendapat 502. Kecil tapi nyata, dan mereka minta diperbaiki tanpa pindah ke infra yang lebih kompleks. Setup blue-green dengan nginx dan systemd adalah solusi yang saya pasang dalam satu sore.

Kenapa bukan Docker atau Kubernetes

Pertanyaan pertama biasanya: kenapa tidak pakai Docker saja? Jawabannya pragmatis. VPS single core dengan 2 GB RAM sudah cukup untuk beban aplikasi mereka. Menambahkan Docker daemon, container registry, dan overhead orchestration untuk sebuah service Node.js yang ukurannya 80 MB bukan keputusan yang bijak di sini. Systemd sudah ada, nginx sudah ada, dan keduanya sudah terbukti stabil bertahun-tahun di production.

Blue-green di level proses OS ini juga lebih mudah di-debug oleh engineer yang belum familiar dengan container. journalctl -u app@blue lebih transparan dari docker logs.

Arsitektur yang saya pakai

Internet → Nginx (port 80/443)

         upstream active

    ┌─────────────────┐
    │  app@blue :3001  │  ← slot aktif
    │  app@green :3002 │  ← slot idle / standby
    └─────────────────┘
         systemd template units

Nginx tidak tahu soal blue atau green. Ia hanya tahu “upstream saat ini ada di port X”. Saat deploy, kita deploy ke port yang idle, verifikasi, lalu ubah ke mana nginx mengarah.

Setup systemd template unit

Template unit systemd pakai karakter @ di nama file. Satu file unit melayani beberapa instance dengan nama yang berbeda.

Buat file /etc/systemd/system/[email protected]:

[Unit]
Description=App instance %i
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/srv/app
Environment=NODE_ENV=production
EnvironmentFile=/srv/app/.env
ExecStart=/usr/bin/node /srv/app/dist/server.js
Restart=on-failure
RestartSec=5

# Port ditentukan lewat environment per-instance
EnvironmentFile=/srv/app/instances/%i.env

[Install]
WantedBy=multi-user.target

Buat direktori dan file environment per instance:

mkdir -p /srv/app/instances

# /srv/app/instances/blue.env
echo "PORT=3001" > /srv/app/instances/blue.env

# /srv/app/instances/green.env
echo "PORT=3002" > /srv/app/instances/green.env

Aktifkan kedua instance:

systemctl daemon-reload
systemctl enable app@blue app@green
systemctl start app@blue
# green sengaja tidak distart dulu — ia slot standby

Cek status:

systemctl status app@blue
journalctl -u app@blue -f

Konfigurasi nginx dengan upstream yang bisa di-swap

Trik utamanya ada di sini. Pisahkan definisi upstream ke file terpisah yang bisa diganti tanpa menyentuh konfigurasi nginx utama.

/etc/nginx/conf.d/app-upstream.conf:

upstream app_active {
    server 127.0.0.1:3001;
    keepalive 32;
}

/etc/nginx/sites-available/app.conf:

include /etc/nginx/conf.d/app-upstream.conf;

server {
    listen 80;
    server_name app.example.com;

    location /health {
        proxy_pass http://app_active;
        proxy_read_timeout 5s;
    }

    location / {
        proxy_pass http://app_active;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache_bypass $http_upgrade;

        # Buffer settings untuk menghindari partial response saat reload
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }
}

Simpan link active ke file state:

echo "blue" > /srv/app/active-slot

Script deploy

Ini script yang saya pakai. Simpan di /srv/app/deploy.sh:

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

APP_DIR="/srv/app"
NGINX_UPSTREAM="/etc/nginx/conf.d/app-upstream.conf"
ACTIVE_SLOT_FILE="$APP_DIR/active-slot"

# 1. Tentukan slot yang akan dipakai untuk versi baru
CURRENT_SLOT=$(cat "$ACTIVE_SLOT_FILE")
if [[ "$CURRENT_SLOT" == "blue" ]]; then
    NEW_SLOT="green"
    NEW_PORT=3002
    OLD_PORT=3001
else
    NEW_SLOT="blue"
    NEW_PORT=3001
    OLD_PORT=3002
fi

echo "Deploy ke slot: $NEW_SLOT (port $NEW_PORT)"
echo "Slot aktif saat ini: $CURRENT_SLOT (port $OLD_PORT)"

# 2. Deploy kode baru ke direktori staging
DEPLOY_DIR="$APP_DIR/releases/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$DEPLOY_DIR"
rsync -az --delete ./dist/ "$DEPLOY_DIR/dist/"
rsync -az package.json "$DEPLOY_DIR/"

# Update symlink
ln -sfn "$DEPLOY_DIR" "$APP_DIR/current"

# 3. Mulai instance baru
systemctl start "app@$NEW_SLOT"
echo "Menunggu instance $NEW_SLOT siap..."

# 4. Health check loop — tunggu hingga ready atau timeout
RETRIES=30
INTERVAL=2
for i in $(seq 1 $RETRIES); do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$NEW_PORT/health" || true)
    if [[ "$STATUS" == "200" ]]; then
        echo "Instance $NEW_SLOT sehat (attempt $i)"
        break
    fi
    if [[ $i -eq $RETRIES ]]; then
        echo "ERROR: Instance $NEW_SLOT tidak merespons setelah $((RETRIES * INTERVAL)) detik"
        systemctl stop "app@$NEW_SLOT"
        exit 1
    fi
    sleep $INTERVAL
done

# 5. Swap upstream nginx
cat > "$NGINX_UPSTREAM" <<EOF
upstream app_active {
    server 127.0.0.1:$NEW_PORT;
    keepalive 32;
}
EOF

nginx -t && nginx -s reload
echo "Nginx di-reload, traffic sekarang ke $NEW_SLOT (port $NEW_PORT)"

# 6. Catat slot aktif yang baru
echo "$NEW_SLOT" > "$ACTIVE_SLOT_FILE"

# 7. Tunggu sebentar lalu matikan slot lama
echo "Menunggu 30 detik sebelum mematikan slot lama ($CURRENT_SLOT)..."
sleep 30

# Cek apakah ada permintaan rollback manual
if [[ "$(cat $ACTIVE_SLOT_FILE)" != "$NEW_SLOT" ]]; then
    echo "Rollback terdeteksi, biarkan $CURRENT_SLOT tetap berjalan"
    exit 0
fi

systemctl stop "app@$CURRENT_SLOT"
echo "Deploy selesai. Active slot: $NEW_SLOT"

Jalankan dengan:

chmod +x /srv/app/deploy.sh
sudo /srv/app/deploy.sh

Rollback

Karena slot lama masih berjalan selama 30 detik setelah swap, rollback semudah:

# Swap upstream kembali ke port lama
cat > /etc/nginx/conf.d/app-upstream.conf <<EOF
upstream app_active {
    server 127.0.0.1:3001;
    keepalive 32;
}
EOF

nginx -s reload
echo "blue" > /srv/app/active-slot

Atau bungkus jadi script rollback.sh yang membaca state dari active-slot dan melakukan kebalikan swap.

Trade-off yang perlu diakui

Kebutuhan RAM dobel. Di window deploy, dua instance berjalan sekaligus. Di VPS 2 GB dengan Node.js yang makan 200-400 MB per proses, ini tidak masalah. Tapi kalau aplikasi Anda butuh 800 MB, kalkulasi ulang sebelum pakai setup ini.

Stateful in-memory tidak bisa langsung. Kalau ada local cache atau in-memory session yang tidak ter-share antara blue dan green, user bisa mendapat respons berbeda setelah swap. Solusinya eksternalisasi state ke Redis atau database sebelum mengadopsi blue-green. Ini bukan kelemahan blue-green, tapi keharusan arsitektur untuk setup apapun yang mau zero-downtime.

Single point of failure di nginx. Kalau nginx turun, kedua slot tidak bisa diakses. Untuk setup VPS single node ini adalah tradeoff yang sudah diterima; kalau Anda butuh HA di layer nginx, itu masuk ke diskusi yang berbeda.

Konfigurasi nginx perlu hak akses. Script deploy perlu bisa menulis ke /etc/nginx/conf.d/ dan menjalankan nginx -s reload. Di production saya pakai sudoers rule yang spesifik:

# /etc/sudoers.d/deploy-nginx
deploy ALL=(ALL) NOPASSWD: /bin/systemctl start app@*, /bin/systemctl stop app@*, /usr/sbin/nginx -t, /usr/sbin/nginx -s reload

Jangan beri akses sudo penuh ke user deploy.

Verdict

Setup ini cocok untuk VPS single node dengan beban aplikasi yang wajar dan tim yang tidak mau overhead Kubernetes atau Docker Compose hanya untuk menghilangkan deploy downtime. Dua syarat utama: aplikasi Anda stateless (atau state ada di external store), dan RAM cukup untuk dua instance berjalan bersamaan.

Kalau kedua syarat itu terpenuhi, waktu setup kurang dari satu jam, rollback di bawah satu menit, dan tidak ada dependency baru yang perlu dipelajari tim. Untuk klien Jakarta tadi, ini sudah berjalan lebih dari enam bulan di production tanpa insiden deploy.

Kalau Anda sudah pakai Docker dan butuh setup serupa di level container, logikanya sama persis — hanya saja unit systemd diganti container, dan swap upstream nginx tetap menjadi mekanisme yang sama.

Ditulis oleh Reza Pradipta