Deployment Zero-Downtime dengan NGINX Blue-Green di VPS Tunggal

Foto oleh Unsplash

Foto oleh Unsplash
Setiap kali Anda merestart kontainer Docker untuk men-deploy versi baru, ada celah — betapapun singkatnya — di mana aplikasi Anda tidak tersedia. Untuk proyek sampingan dengan traffic rendah, celah itu masih bisa ditoleransi. Untuk layanan produksi dengan pengguna nyata dan SLA, tidak. Saya mengimplementasikan blue-green deployment menggunakan NGINX dan Docker biasa di DigitalOcean Droplet setelah seorang klien mengeluh tentang gangguan singkat selama siklus rilis nightly kami. Solusinya tidak membutuhkan Kubernetes, load balancer eksternal, atau tooling berbayar — hanya bash script, dua slot kontainer, dan NGINX reload yang cerdas.
Blue-green deployment menjaga dua lingkungan produksi yang identik — disebut Blue dan Green — selalu tersedia. Setiap saat, satu lingkungan aktif (melayani traffic pengguna) dan yang lain idle (siap menerima deployment berikutnya). Ketika Anda men-deploy versi baru, Anda menjalankannya di slot idle, menjalankan health check, lalu membalik upstream NGINX untuk mengarah ke slot baru. Slot lama tetap berjalan untuk rollback instan — jika ada yang terlihat salah setelah pembalikan, Anda mengubah upstream kembali dan memuat ulang NGINX. Seluruh cutover memakan waktu kurang dari satu detik.
Perilaku restart default Docker (`docker stop` + `docker start`) menciptakan jendela downtime. Bahkan dengan startup yang cepat, biasanya ada 2-10 detik di mana tidak ada kontainer yang mendengarkan di port. NGINX mengembalikan 502 Bad Gateway selama jendela ini. `depends_on` Docker Compose dengan health check membantu, tetapi graceful shutdown kontainer lama dan kesiapan kontainer baru tidak tersinkronisasi secara atomis. Blue-green menghindari ini dengan tidak pernah menghentikan kontainer aktif sampai yang baru dikonfirmasi sehat dan melayani traffic.
Perintah `nginx -s reload` NGINX memuat ulang file konfigurasi dan memperbarui target upstream tanpa menjatuhkan koneksi yang sedang berlangsung. Ketika Anda mengubah blok upstream untuk mengarah ke port kontainer baru lalu memuatnya, NGINX menyelesaikan semua permintaan aktif di upstream lama sebelum mengalihkan koneksi baru ke yang baru. Perilaku ini — disebut graceful reload — adalah kunci yang membuat zero-downtime blue-green menjadi mungkin dengan NGINX vanilla tanpa plugin atau modul berbayar.
┌──────────────────────────────────────────────────────────────────┐
│ Blue-Green Deployment Flow with NGINX │
│ │
│ Internet │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ NGINX Upstream (upstream.conf) │ │
│ │ │ │
│ │ Step 1: upstream → app-blue:3000 │ │
│ │ Step 2: deploy app-green:3001 │ │
│ │ Step 3: health check green ✓ │ │
│ │ Step 4: upstream → app-green:3001 │ │
│ │ Step 5: reload nginx (0 downtime) │ │
│ └──────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ app-blue │ │ app-green │ │
│ │ :3000 │ │ :3001 │ │
│ │ (v1.0) │ │ (v2.0 new) │ │
│ │ IDLE after │ │ LIVE after │ │
│ │ cutover │ │ cutover │ │
│ └─────────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Tambahkan `proxy_next_upstream error timeout http_502 http_503` ke blok location NGINX Anda. Ini memberitahu NGINX untuk secara otomatis mencoba ulang permintaan di upstream server berikutnya jika upstream utama mengembalikan error atau timeout. Dikombinasikan dengan menjaga kedua kontainer blue dan green berjalan selama jendela transisi, ini memastikan bahkan in-flight request pada saat pembalikan upstream ditangani secara transparan.
Inti dari setup blue-green adalah bash script yang: mendeteksi kontainer mana yang sedang live (blue atau green), memulai kontainer lain dengan image baru, melakukan polling endpoint health check sampai merespons 200, memperbarui file upstream NGINX, memuat ulang NGINX secara graceful, lalu menghentikan kontainer yang sebelumnya live. Script ini idempoten — Anda bisa menjalankannya beberapa kali dengan aman. Script keluar dengan kode non-zero jika health check gagal, membiarkan kontainer lama berjalan dan NGINX tidak berubah, sehingga tidak ada pengguna yang pernah melihat deployment yang rusak.
Endpoint health check harus mencerminkan kesiapan aplikasi yang sebenarnya, bukan hanya startup HTTP server. Endpoint /health yang baik di aplikasi Node.js/Express memeriksa konektivitas database, ketersediaan cache, dan koneksi layanan eksternal yang diperlukan. Jika dependensi apa pun tidak tersedia, health check mengembalikan HTTP 503, script deployment melihat kegagalan, dan pertukaran blue-green dibatalkan. Ini berarti migrasi database yang gagal atau environment variable yang salah dikonfigurasi akan mencegah deployment buruk tersebut menerima traffic live.
#!/bin/bash
# blue-green-deploy.sh — Zero-downtime deploy script for NGINX + Docker
set -euo pipefail
IMAGE="registry.example.com/myapp"
TAG="${1:-latest}"
NGINX_UPSTREAM="/etc/nginx/conf.d/upstream.conf"
# Detect active color
if docker ps --format '{{.Names}}' | grep -q "app-blue"; then
ACTIVE="blue"; ACTIVE_PORT=3000
IDLE="green"; IDLE_PORT=3001
else
ACTIVE="green"; ACTIVE_PORT=3001
IDLE="blue"; IDLE_PORT=3000
fi
echo "Active: app-$ACTIVE ($ACTIVE_PORT) → deploying to app-$IDLE ($IDLE_PORT)"
# 1. Pull new image
docker pull "$IMAGE:$TAG"
# 2. Start idle container
docker rm -f "app-$IDLE" 2>/dev/null || true
docker run -d --name "app-$IDLE" --network app-net -p "$IDLE_PORT:3000" -e NODE_ENV=production "$IMAGE:$TAG"
# 3. Health-check loop (30s timeout)
echo "Waiting for app-$IDLE to become healthy..."
for i in $(seq 1 30); do
if curl -sf "http://localhost:$IDLE_PORT/health" > /dev/null; then
echo "Healthy after ${i}s"; break
fi
[ "$i" -eq 30 ] && { echo "Health check failed"; exit 1; }
sleep 1
done
# 4. Flip NGINX upstream
cat > "$NGINX_UPSTREAM" <<EOF
upstream app_backend {
server 127.0.0.1:$IDLE_PORT;
}
EOF
nginx -t && nginx -s reload
echo "Traffic switched to app-$IDLE"
# 5. Stop old container
docker stop "app-$ACTIVE" && docker rm "app-$ACTIVE"
echo "Deployment complete. Active: app-$IDLE"
# /etc/nginx/sites-enabled/myapp.conf
# server {
# listen 80;
# server_name myapp.example.com;
# location / {
# proxy_pass http://app_backend;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# }
# }Keindahan blue-green adalah rollback yang instan dan tidak memerlukan deployment baru. Karena kontainer lama masih berjalan setelah cutover (hanya tidak menerima koneksi baru via NGINX), rollback cukup dengan mengarahkan upstream NGINX kembali ke port lama dan memuat ulang. Tambahkan script `rollback.sh` yang mencerminkan script deploy namun melewati startup kontainer — cukup balik upstream dan reload. Biarkan kontainer lama berjalan setidaknya 30 menit setelah deploy yang sukses sebelum menghapusnya, memberi Anda jendela rollback yang bersih.
Jika aplikasi Anda mempertahankan koneksi WebSocket atau long-polling, graceful reload NGINX tidak akan secara paksa mengakhiri koneksi tersebut — mereka tetap di upstream lama sampai klien memutuskan koneksi. Ini biasanya diinginkan, tetapi berarti kontainer lama mungkin terus menangani traffic selama beberapa menit setelah pertukaran. Jangan hentikan kontainer lama segera setelah reload NGINX. Sebaiknya tunggu setidaknya 60 detik (atau monitor koneksi aktif dengan `ss -tnp | grep <port>`) sebelum menghentikan kontainer lama untuk menghindari pemutusan paksa sesi WebSocket aktif.
Script deployment terintegrasi dengan baik ke GitHub Actions menggunakan marketplace action `appleboy/ssh-action`. Workflow CI Anda membangun dan mendorong image Docker di setiap push ke main, lalu SSH ke server produksi dan menjalankan `./blue-green-deploy.sh <image-tag>`. Workflow gagal jika loop health check timeout, mengirim notifikasi kegagalan ke Slack atau email sebelum kode yang rusak mencapai pengguna. Simpan private key SSH dan IP server di GitHub Actions secrets — jangan pernah hard-code di file workflow.
Setup blue-green ini berjalan di Droplet DigitalOcean $12-24/bulan tunggal tanpa infrastruktur tambahan. Kubernetes dengan GKE atau DOKS untuk jaminan zero-downtime yang sama biayanya setidaknya $35-75/bulan untuk control plane saja, belum termasuk biaya node. Untuk workload kecil hingga menengah di mana Anda mengontrol seluruh stack, pendekatan bash + NGINX + Docker secara dramatis lebih murah dan lebih mudah di-debug. Kubernetes menjadi sepadan dengan kompleksitas operasionalnya ketika Anda membutuhkan horizontal Pod autoscaling, isolasi namespace multi-tenant, atau jaminan resource tingkat cluster — tidak satu pun dari ini yang dibutuhkan deployment VPS single-service.