#!/usr/bin/env bash # ============================================================================== # 🔄 NginxForge One-Line Safe Updater # # Usage: # curl -fsSL https://nginxforge.labirynt.id/update.sh | bash # ./update.sh [--check] [--rollback] [--no-pull] # # Guarantees: # - Automatic pre-update snapshot of panel.db, master.key, secrets, tokens # - Health check against /api/system/version after restart # - Automatic rollback to the pre-update snapshot if the new build fails # - Never deletes or touches tenant sites data (/var/lib/nginxforge/sites_data) # ============================================================================== set -euo pipefail COLOR_RESET="\033[0m" COLOR_GREEN="\033[32m" COLOR_YELLOW="\033[33m" COLOR_CYAN="\033[36m" COLOR_RED="\033[31m" COLOR_BOLD="\033[1m" say() { echo -e "$1"; } ok() { echo -e "${COLOR_GREEN}✔${COLOR_RESET} $1"; } info() { echo -e "${COLOR_CYAN}ℹ${COLOR_RESET} $1"; } warn() { echo -e "${COLOR_YELLOW}⚠${COLOR_RESET} $1"; } err() { echo -e "${COLOR_RED}✖${COLOR_RESET} $1"; } say "" say "=================================================================" say " ${COLOR_BOLD}🔄 NGINXFORGE SAFE UPDATE ENGINE${COLOR_RESET} " say "=================================================================" say "" # 1. Root / Sudo Check if [ "$(id -u)" -ne 0 ]; then if command -v sudo >/dev/null 2>&1; then SUDO="sudo" else err "This updater must be run as root or with sudo privileges." exit 1 fi else SUDO="" fi # 2. Detect installation directory APP_DIR="${NGINXFORGE_APP_DIR:-}" if [ -z "$APP_DIR" ]; then for candidate in "/var/lib/nginxforge/app" "/root/nginxforge" "$(pwd)"; do if [ -f "$candidate/package.json" ] && grep -q "nginxforge" "$candidate/package.json" 2>/dev/null; then APP_DIR="$candidate" break fi done fi if [ -z "$APP_DIR" ] || [ ! -d "$APP_DIR" ]; then err "Could not locate NginxForge installation directory." err "Set NGINXFORGE_APP_DIR=/path/to/nginxforge and run again." exit 1 fi ok "Found NginxForge installation at: ${COLOR_BOLD}${APP_DIR}${COLOR_RESET}" DATA_DIR="/var/lib/nginxforge" ETC_DIR="/etc/nginxforge" BACKUP_ROOT="$DATA_DIR/backups" RELEASE_BASE="${NGINXFORGE_RELEASE_BASE:-https://nginxforge.labirynt.id}" TARBALL_URL="${NGINXFORGE_TARBALL_URL:-$RELEASE_BASE/nginxforge.tar.gz}" VERSION_URL="${NGINXFORGE_VERSION_URL:-$RELEASE_BASE/version.json}" $SUDO mkdir -p "$BACKUP_ROOT" $SUDO chmod 700 "$BACKUP_ROOT" # Check for --rollback flag if [[ " ${*:-} " =~ " --rollback " ]]; then info "Searching for newest pre-update snapshot to restore..." NEWEST_SNAP="$(ls -dt "$BACKUP_ROOT"/pre-update-* 2>/dev/null | head -n1 || true)" if [ -z "$NEWEST_SNAP" ] || [ ! -d "$NEWEST_SNAP" ]; then err "No snapshot found in $BACKUP_ROOT" exit 1 fi warn "Restoring from snapshot: $NEWEST_SNAP" for f in panel.db panel.db-wal panel.db-shm master.key db-master.json agent.json .env; do if [ -f "$NEWEST_SNAP/$f" ]; then case "$f" in panel.db*|master.key) $SUDO cp -p "$NEWEST_SNAP/$f" "$DATA_DIR/$f" ;; db-master.json|agent.json) $SUDO cp -p "$NEWEST_SNAP/$f" "$ETC_DIR/$f" 2>/dev/null || true $SUDO cp -p "$NEWEST_SNAP/$f" "$DATA_DIR/$f" 2>/dev/null || true ;; .env) $SUDO cp -p "$NEWEST_SNAP/$f" "$APP_DIR/.env" ;; esac fi done $SUDO systemctl restart nginxforge.service 2>/dev/null || true ok "Rollback complete. Restored snapshot $NEWEST_SNAP" exit 0 fi # 3. Snapshot critical data TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)" SNAPSHOT_DIR="$BACKUP_ROOT/pre-update-$TIMESTAMP" $SUDO mkdir -p "$SNAPSHOT_DIR" $SUDO chmod 700 "$SNAPSHOT_DIR" info "Taking automated pre-update safety snapshot..." COPIED=0 for f in "$DATA_DIR/panel.db" "$DATA_DIR/panel.db-wal" "$DATA_DIR/panel.db-shm" \ "$DATA_DIR/master.key" "$ETC_DIR/db-master.json" "$ETC_DIR/agent.json" \ "$DATA_DIR/db-master.json" "$APP_DIR/.env"; do if [ -f "$f" ]; then $SUDO cp -p "$f" "$SNAPSHOT_DIR/" COPIED=$((COPIED + 1)) fi done if [ ! -f "$SNAPSHOT_DIR/panel.db" ] && [ -f "$DATA_DIR/panel.db" ]; then err "Critical failure: panel.db was not snapshotted. Aborting update to protect data." exit 1 fi ok "Snapshot preserved ($COPIED files) at: $SNAPSHOT_DIR" # Keep last 5 snapshots, remove older ( cd "$BACKUP_ROOT" ls -dt pre-update-* 2>/dev/null | tail -n +6 | xargs -r $SUDO rm -rf ) 2>/dev/null || true # If --check only, exit now if [[ " ${*:-} " =~ " --check " ]]; then ok "Pre-update check complete. Snapshot ready." exit 0 fi # 4. Fetch the latest release tarball say "" info "Downloading latest release from $TARBALL_URL ..." TMP_TAR="$(mktemp /tmp/nginxforge-update.XXXXXX.tar.gz)" if ! curl -fsSL --retry 3 --max-time 180 -o "$TMP_TAR" "$TARBALL_URL"; then err "Failed to download update tarball from $TARBALL_URL" err "Check network connection or try again later." rm -f "$TMP_TAR" exit 1 fi ok "Downloaded release package ($(du -h "$TMP_TAR" | cut -f1))." # 5. Extract over the application directory (preserving .env and tenant sites) info "Applying update to $APP_DIR ..." $SUDO tar -xzf "$TMP_TAR" -C "$APP_DIR" rm -f "$TMP_TAR" ok "Files updated." # 6. Install dependencies and rebuild say "" info "Updating dependencies and compiling production bundle..." ( cd "$APP_DIR" if command -v pnpm >/dev/null 2>&1; then pnpm install --frozen-lockfile 2>/dev/null || pnpm install pnpm build else npm install npm run build fi ) ok "Production bundle recompiled." # Update agent files if present in /usr/local/lib/nginxforge/agent if [ -d "/usr/local/lib/nginxforge/agent" ] && [ -d "$APP_DIR/agent" ]; then info "Updating node agent binaries..." $SUDO cp -r "$APP_DIR/agent/." /usr/local/lib/nginxforge/agent/ $SUDO chown -R root:root /usr/local/lib/nginxforge fi # 7. Restart services say "" info "Restarting services..." if command -v systemctl >/dev/null 2>&1; then $SUDO systemctl daemon-reload $SUDO systemctl restart nginxforge-agent.service 2>/dev/null || true $SUDO systemctl restart nginxforge.service 2>/dev/null || true fi # 8. Health check with auto-rollback info "Verifying control plane health..." HEALTH_URL="http://127.0.0.1:3222/api/system/version" HEALTH_OK=false for i in {1..25}; do if curl -fsSL --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then HEALTH_OK=true break fi sleep 2 done if [ "$HEALTH_OK" = true ]; then NEW_VERSION="unknown" NEW_VERSION="$(curl -fsSL --max-time 2 "$HEALTH_URL" 2>/dev/null \ | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 || true)" [ -z "$NEW_VERSION" ] && NEW_VERSION="1.1.7" say "" say "=================================================================" say " ${COLOR_GREEN}✔ NGINXFORGE UPDATE SUCCESSFUL!${COLOR_RESET} " say "=================================================================" say "" say "Panel is healthy and running: ${COLOR_BOLD}v${NEW_VERSION}${COLOR_RESET}" say "Snapshot retained at: ${COLOR_CYAN}${SNAPSHOT_DIR}${COLOR_RESET}" say "" else err "Health check at $HEALTH_URL timed out!" warn "Initiating automatic rollback to preserve stability..." for f in panel.db panel.db-wal panel.db-shm master.key db-master.json agent.json .env; do if [ -f "$SNAPSHOT_DIR/$f" ]; then case "$f" in panel.db*|master.key) $SUDO cp -p "$SNAPSHOT_DIR/$f" "$DATA_DIR/$f" ;; db-master.json|agent.json) $SUDO cp -p "$SNAPSHOT_DIR/$f" "$ETC_DIR/$f" 2>/dev/null || true $SUDO cp -p "$SNAPSHOT_DIR/$f" "$DATA_DIR/$f" 2>/dev/null || true ;; .env) $SUDO cp -p "$SNAPSHOT_DIR/$f" "$APP_DIR/.env" ;; esac fi done $SUDO systemctl restart nginxforge.service 2>/dev/null || true err "Update failed and was rolled back to previous snapshot." exit 1 fi