Tantangan Upgrade In-Place Daemon Manajemen Server

Daemon web UI seperti panel administrasi infrastruktur (misalnya arsitektur panel 3x-ui) menggabungkan binary server, aset web statis, dan embedded database seperti SQLite dalam satu lingkungan eksekusi host. Melakukan pembaruan langsung pada host aktif (in-place upgrade) membawa risiko tinggi: binary baru bisa mengalami crash-loop, migrasi skema database gagal di tengah jalan, atau aset web baru tidak cocok dengan versi API backend.

Jika pembaruan gagal tanpa mekanisme recovery otomatis, akses administratif server akan terputus seketika. Pola safe rollback & state backup mengatasi masalah ini dengan memastikan status sistem lama selalu dicadangkan sebelum perubahan dieksekusi, status baru diuji secara otomatis melalui endpoint health check, dan seluruh operasi dikembalikan ke status awal jika pengujian tidak lolos verifikasi.

Arsitektur State, Binary, dan Konfigurasi

Pemisahan direktori dan pemanfaatan symlink adalah fondasi dari in-place update yang aman. Jangan pernah menimpa binary yang sedang berjalan secara langsung. Terapkan pemisahan stateful data dan stateless binaries:

  • Stateless Binary & Assets: Tempatkan rilis dalam direktori berversi, misalnya /opt/ui-daemon/releases/vX.Y.Z, lalu arahkan ke service runtime melalui symlink /opt/ui-daemon/current.
  • Stateful Data: Database SQLite dan file konfigurasi disimpan di luar direktori rilis, misalnya /etc/ui-daemon/ atau /var/lib/ui-daemon/, dan diakses oleh binary melalui path persisten atau symlink.

Penanganan SQLite Schema Migration

SQLite rentan terkunci atau mengalami corrupt sebagian jika proses dihentikan mendadak saat menjalankan ALTER TABLE atau transaksi DDL. Sebelum service versi baru dijalankan, dua langkah wajib dilakukan:

  1. Snapshot file database menggunakan API backup SQLite atau utilitas CLI sqlite3 .backup guna mencegah partial write saat proses daemon lama sedang ditutup (flush WAL).
  2. Pemeriksaan integritas database menggunakan pragma: PRAGMA integrity_check;.

Implementasi Skrip Upgrade Idempoten dan Otomatis

Skrip berikut mengotomatiskan alur: backup database, ekstraksi rilis baru, perpindahan symlink, restart unit systemd, pengecekan health check HTTP via cURL, serta rollback instan jika health check gagal.

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

APP_NAME="ui-daemon"
BASE_DIR="/opt/${APP_NAME}"
DATA_DIR="/var/lib/${APP_NAME}"
DB_FILE="${DATA_DIR}/${APP_NAME}.db"
CURRENT_LINK="${BASE_DIR}/current"
BACKUP_DIR="/var/backups/${APP_NAME}"
NEW_VERSION="$1" # Argumen: versi baru, misal: v2.1.0
NEW_RELEASE_DIR="${BASE_DIR}/releases/${NEW_VERSION}"
HEALTH_CHECK_URL="http://127.0.0.1:2053/api/health"

if [ -z "${NEW_VERSION}" ]; then
    echo "Error: Argumen versi rilis diperlukan." >&2
    exit 1
fi

mkdir -p "${BACKUP_DIR}" "${NEW_RELEASE_DIR}"

# 1. Simpan target rilis sebelumnya untuk target rollback
PREV_TARGET=""
if [ -L "${CURRENT_LINK}" ]; then
    PREV_TARGET=$(readlink -f "${CURRENT_LINK}")
fi

# 2. Backup database SQLite menggunakan online backup
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DB_BACKUP_PATH="${BACKUP_DIR}/${APP_NAME}_${TIMESTAMP}.db"

if [ -f "${DB_FILE}" ]; then
    echo "[*] Memvalidasi dan mencadangkan SQLite database..."
    INTEGRITY_CHECK=$(sqlite3 "${DB_FILE}" "PRAGMA integrity_check;" 2>&1 || true)
    if [ "${INTEGRITY_CHECK}" != "ok" ]; then
        echo "[!] Database korup sebelum upgrade. Batalkan proses! Detail: ${INTEGRITY_CHECK}" >&2
        exit 1
    fi
    sqlite3 "${DB_FILE}" ".backup '${DB_BACKUP_PATH}'"
fi

# Definisi fungsi rollback instan
rollback() {
    echo "[!] Upgrade gagal. Menginisiasi rollback otomatis..." >&2
    
    if [ -n "${PREV_TARGET}" ] && [ -d "${PREV_TARGET}" ]; then
        ln -sfn "${PREV_TARGET}" "${CURRENT_LINK}"
    fi

    if [ -f "${DB_BACKUP_PATH}" ]; then
        echo "[*] Memulihkan database SQLite dari snapshot..."
        systemctl stop "${APP_NAME}" || true
        cp -f "${DB_BACKUP_PATH}" "${DB_FILE}"
    fi

    systemctl restart "${APP_NAME}" || true
    echo "[!] Rollback selesai. Menampilkan log eror terakhir:" >&2
    journalctl -u "${APP_NAME}" -n 20 --no-pager >&2
    exit 1
}

# Pasang trap: jika skrip terminated sebelum selesai, panggil rollback
trap rollback ERR

# 3. Download/Unpack binary dan aset baru (simulasi ekstraksi direktori)
echo "[*] Mempersiapkan rilis ${NEW_VERSION}..."
# Asumsi binary di-download atau dipindahkan ke NEW_RELEASE_DIR
# tar -xzf /tmp/${NEW_VERSION}.tar.gz -C "${NEW_RELEASE_DIR}"

# 4. Atomic switch symlink binary
echo "[*] Mengarahkan symlink ke versi baru..."
ln -sfn "${NEW_RELEASE_DIR}" "${CURRENT_LINK}"

# 5. Restart daemon melalui systemd
echo "[*] Memulai ulang systemd unit..."
systemctl daemon-reload
systemctl restart "${APP_NAME}"

# 6. Observasi dan Health Check pasca upgrade
echo "[*] Menjalankan validasi health check..."
HEALTHY=false
for i in {1..10}; do
    STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${HEALTH_CHECK_URL}" || true)
    if [ "${STATUS_CODE}" -eq 200 ]; then
        HEALTHY=true
        break
    fi
    sleep 2
done

if [ "${HEALTHY}" = false ]; then
    echo "[!] Service gagal merespons HTTP 200 pada ${HEALTH_CHECK_URL}" >&2
    rollback
fi

# Upgrade tuntas tanpa kesalahan: bersihkan trap
trap - ERR
echo "[+] Upgrade ke ${NEW_VERSION} sukses divalidasi."

Konfigurasi Systemd untuk Mencegah Crash-Loop

File unit systemd harus dirancang agar membatasi restart tak terbatas saat binary crash-loop dan mereferensikan symlink yang konsisten:

[Unit]
Description=Server Management UI Daemon
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/ui-daemon/current
ExecStart=/opt/ui-daemon/current/ui-daemon --config /etc/ui-daemon/config.json
Restart=on-failure
RestartSec=3s
StartLimitIntervalSec=60s
StartLimitBurst=3
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Pengaturan StartLimitIntervalSec=60s dan StartLimitBurst=3 memastikan systemd berhenti mencoba restart jika daemon mengalami crash berulang kali dalam kurun 60 detik. Hal ini mencegah resource starvation pada CPU dan I/O disk.

Observasi Log dan Debugging

Saat upgrade gagal atau health check timeout, debugging instan dilakukan menggunakan journalctl dengan parameter terfilter:

  • Lihat log error spesifik sejak percobaan boot terakhir: journalctl -u ui-daemon.service -b -p err --no-pager
  • Streaming log daemon secara realtime: journalctl -u ui-daemon.service -f -n 50
  • Cek SQLite journal yang tertinggal (WAL/SHM): jika terdapat .db-wal setelah service berhenti paksa, pastikan binary versi lama mampu membaca WAL tersebut atau lakukan checkpoint manual sebelum me-restore backup.

Catatan: Jika aplikasi backend menerapkan migrasi skema yang bersifat backward-incompatible (misal: menghapus kolom yang dibutuhkan versi lama), proses rollback binary ke versi sebelumnya tanpa me-restore file database fisik dapat memicu runtime error seketika. Selalu sertakan restore file database saat skema telah tersentuh migrator.