mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
- install.sh: staged installer with pre-flight checks (disk/memory/root), apt/dnf lock handling, low-RAM safe mode + swap, source-or-release install modes, generated secrets/.env, self-signed cert bootstrap, nginx + systemd wiring, a post-install health probe with automatic rollback, and optional Let's Encrypt issuance for the panel domain. - scripts/update.sh: in-place updater with release self-refresh, pre-update database + tree backups, release/source paths, health-checked rollback, and backup pruning. - scripts/build-release.sh: builds a portable release tarball (pre-built venv + frontend) at the repo root. - serverkit CLI: 'update' delegates to scripts/update.sh and accepts --release [vX.Y.Z]. - Harden the serverkit-agent unit (kernel/namespace protections, MemoryMax, LimitNOFILE) and ignore release tarball artifacts.
342 lines
14 KiB
Bash
342 lines
14 KiB
Bash
#!/bin/bash
|
||
#
|
||
# ServerKit updater.
|
||
#
|
||
# Pulls the newest code (or release tarball), rebuilds, and restarts the
|
||
# stack while preserving the database, generated secrets, and .env. A failed
|
||
# health check rolls the previous tree back into place.
|
||
#
|
||
# bash /opt/serverkit/scripts/update.sh
|
||
# INSTALL_FROM_RELEASE=1 bash /opt/serverkit/scripts/update.sh
|
||
# SERVERKIT_VERSION=v1.7.0 bash /opt/serverkit/scripts/update.sh
|
||
#
|
||
set -euo pipefail
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Terminal styling (violet ServerKit identity, degrades to plain text)
|
||
# ---------------------------------------------------------------------------
|
||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
|
||
ESC=$'\033'
|
||
RST="${ESC}[0m"; BLD="${ESC}[1m"
|
||
paint() { printf '%s[38;2;%d;%d;%dm' "$ESC" "$1" "$2" "$3"; }
|
||
else
|
||
RST=''; BLD=''
|
||
paint() { :; }
|
||
fi
|
||
|
||
V3="$(paint 139 92 246)"; V4="$(paint 124 58 237)"
|
||
PAPER="$(paint 237 233 254)"; FOG="$(paint 113 108 140)"
|
||
HUE_OK="$(paint 52 211 153)"; HUE_WARN="$(paint 250 204 21)"
|
||
HUE_ERR="$(paint 248 113 113)"; HUE_LINK="$(paint 103 232 249)"
|
||
|
||
good() { printf ' %s✔%s %s\n' "$HUE_OK" "$RST" "$1"; }
|
||
warn() { printf ' %s▴%s %s\n' "$HUE_WARN" "$RST" "$1"; }
|
||
halt() { printf ' %s✘%s %s\n' "$HUE_ERR" "$RST" "$1" >&2; exit 1; }
|
||
step() { printf ' %s❯%s %s\n' "$HUE_LINK" "$RST" "$1"; }
|
||
|
||
STARTED_AT=0
|
||
PHASE_N=0
|
||
clock() {
|
||
[ "$STARTED_AT" -gt 0 ] || { printf ''; return; }
|
||
local secs=$(( $(date +%s) - STARTED_AT ))
|
||
printf '%dm %02ds' "$((secs / 60))" "$((secs % 60))"
|
||
}
|
||
phase() {
|
||
PHASE_N=$((PHASE_N + 1))
|
||
printf '\n %s%s%02d%s %s%s%s %s%s%s\n' \
|
||
"$BLD" "$V3" "$PHASE_N" "$RST" "$BLD" "$1" "$RST" "$FOG" "$(clock)" "$RST"
|
||
printf ' %s%s%s\n\n' "$V4" "──────────────────────────────────────" "$RST"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Settings
|
||
# ---------------------------------------------------------------------------
|
||
INSTALL_DIR="/opt/serverkit"
|
||
VENV_DIR="$INSTALL_DIR/venv"
|
||
BACKUP_DIR="/var/backups/serverkit"
|
||
GITHUB_REPO="${GITHUB_REPO:-jhd3197/ServerKit}"
|
||
INSTALL_FROM_RELEASE="${INSTALL_FROM_RELEASE:-0}"
|
||
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-0}"
|
||
SERVERKIT_VERSION="${SERVERKIT_VERSION:-}"
|
||
BACKEND_SERVICE="serverkit"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Sanity checks
|
||
# ---------------------------------------------------------------------------
|
||
[ "$EUID" -eq 0 ] || halt "Run as root."
|
||
[ -d "$INSTALL_DIR" ] || halt "ServerKit is not installed at $INSTALL_DIR"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pick install mode: release tarball vs. source tree
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$INSTALL_FROM_RELEASE" != "1" ] && [ ! -d "$INSTALL_DIR/backend/src" ]; then
|
||
step "No source tree found — switching to release download."
|
||
INSTALL_FROM_RELEASE=1
|
||
fi
|
||
[ "$BUILD_FROM_SOURCE" = "1" ] && INSTALL_FROM_RELEASE=0
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Self-refresh: replace this script with the release copy and re-exec, so an
|
||
# old updater can't drive a new release. Runs once (guarded by SELF_REFRESHED)
|
||
# and only in release mode. Kept inline so $@ / BASH_SOURCE resolve correctly.
|
||
# ---------------------------------------------------------------------------
|
||
if [ "${SELF_REFRESHED:-0}" != "1" ] && [ "$INSTALL_FROM_RELEASE" = "1" ] \
|
||
&& [ "${SERVERKIT_NO_SELF_REFRESH:-0}" != "1" ]; then
|
||
newest_tag=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null \
|
||
| grep -m1 '"tag_name"' | cut -d'"' -f4 || true)
|
||
if [ -n "$newest_tag" ]; then
|
||
upstream="https://raw.githubusercontent.com/${GITHUB_REPO}/${newest_tag}/scripts/update.sh"
|
||
tmp=$(mktemp)
|
||
if curl -fsSL "$upstream" -o "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
||
if ! cmp -s "$tmp" "${BASH_SOURCE[0]}"; then
|
||
step "Refreshing update.sh from $newest_tag..."
|
||
cp "$tmp" "${BASH_SOURCE[0]}" 2>/dev/null || true
|
||
rm -f "$tmp"
|
||
export SELF_REFRESHED=1
|
||
exec bash "${BASH_SOURCE[0]}" "$@"
|
||
fi
|
||
fi
|
||
rm -f "$tmp"
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Backfill the encryption key for installs that pre-date secrets-at-rest
|
||
# ---------------------------------------------------------------------------
|
||
backfill_encryption_key() {
|
||
local env_file="$INSTALL_DIR/.env"
|
||
if [ -f "$env_file" ] && ! grep -q '^SERVERKIT_ENCRYPTION_KEY=' "$env_file"; then
|
||
step "Generating SERVERKIT_ENCRYPTION_KEY..."
|
||
local key
|
||
key=$(python3 -c 'import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')
|
||
echo "SERVERKIT_ENCRYPTION_KEY=$key" >> "$env_file"
|
||
good "Encryption key added to $env_file"
|
||
fi
|
||
}
|
||
backfill_encryption_key
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Header
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n %s%sServerKit Updater%s\n' "$BLD" "$PAPER" "$RST"
|
||
STARTED_AT=$(date +%s)
|
||
|
||
CURRENT_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null | tr -d '\n\r ' || echo "unknown")
|
||
step "Current version: $CURRENT_VERSION"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pull latest source (source-mode only)
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$INSTALL_FROM_RELEASE" != "1" ] && [ -d "$INSTALL_DIR/.git" ]; then
|
||
phase "Syncing Repository"
|
||
cd "$INSTALL_DIR"
|
||
git stash -q 2>/dev/null || true
|
||
if git fetch --depth=1 origin main 2>/dev/null; then
|
||
git reset --hard FETCH_HEAD 2>&1 | tail -1
|
||
good "Repository synced to origin/main"
|
||
else
|
||
warn "Repo sync failed — keeping the existing source."
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Back up database + install tree
|
||
# ---------------------------------------------------------------------------
|
||
phase "Database Backup"
|
||
mkdir -p "$BACKUP_DIR"
|
||
|
||
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
|
||
if [ -f "$DB_FILE" ]; then
|
||
BACKUP_FILE="$BACKUP_DIR/serverkit-pre-upgrade-$(date +%Y%m%d-%H%M%S).db"
|
||
if cp "$DB_FILE" "$BACKUP_FILE"; then
|
||
good "Database backed up to $BACKUP_FILE"
|
||
else
|
||
halt "Database backup failed — aborting upgrade."
|
||
fi
|
||
else
|
||
warn "No SQLite database at $DB_FILE — skipping DB backup."
|
||
fi
|
||
|
||
BACKUP_TREE="$BACKUP_DIR/serverkit-tree-$(date +%Y%m%d-%H%M%S)"
|
||
mkdir -p "$BACKUP_TREE"
|
||
rsync -a --exclude=venv --exclude=backups --exclude=node_modules \
|
||
"$INSTALL_DIR/" "$BACKUP_TREE/" 2>/dev/null || \
|
||
cp -a "$INSTALL_DIR" "$BACKUP_TREE" 2>/dev/null || true
|
||
good "Install tree backed up."
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Stop the stack
|
||
# ---------------------------------------------------------------------------
|
||
phase "Stopping Services"
|
||
systemctl stop "$BACKEND_SERVICE" 2>/dev/null || true
|
||
systemctl stop nginx 2>/dev/null || true
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Deploy: download a release, or rebuild from source
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$INSTALL_FROM_RELEASE" = "1" ]; then
|
||
phase "Downloading Release"
|
||
|
||
case "$(uname -m)" in
|
||
x86_64) DL_ARCH="amd64" ;;
|
||
aarch64|arm64) DL_ARCH="arm64" ;;
|
||
*) halt "Unsupported architecture: $(uname -m)" ;;
|
||
esac
|
||
|
||
if [ -n "$SERVERKIT_VERSION" ]; then
|
||
RELEASE_TAG="$SERVERKIT_VERSION"
|
||
step "Pinned release: $RELEASE_TAG"
|
||
else
|
||
step "Looking up the latest release..."
|
||
RELEASE_TAG=$(curl -sf "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \
|
||
| grep '"tag_name"' | head -1 | cut -d'"' -f4)
|
||
[ -n "$RELEASE_TAG" ] || halt "Could not determine the latest release."
|
||
good "Latest release: $RELEASE_TAG"
|
||
fi
|
||
|
||
BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}"
|
||
TARBALL="/tmp/serverkit-${RELEASE_TAG}-linux-${DL_ARCH}.tar.gz"
|
||
|
||
step "Downloading release tarball (${DL_ARCH})..."
|
||
curl -sfL "${BASE_URL}/serverkit-${RELEASE_TAG}-linux-${DL_ARCH}.tar.gz" -o "$TARBALL" \
|
||
|| halt "Failed to download the release tarball."
|
||
|
||
step "Unpacking release..."
|
||
STAGE="/tmp/serverkit-release-$$"
|
||
rm -rf "$STAGE"
|
||
mkdir -p "$STAGE"
|
||
tar xzf "$TARBALL" -C "$STAGE"
|
||
rm -f "$TARBALL"
|
||
|
||
# Carry the live .env and database across.
|
||
cp "$INSTALL_DIR/.env" "$STAGE/serverkit/.env" 2>/dev/null || true
|
||
cp "$INSTALL_DIR/backend/instance/serverkit.db" \
|
||
"$STAGE/serverkit/backend/instance/serverkit.db" 2>/dev/null || true
|
||
|
||
mv "$INSTALL_DIR" "$INSTALL_DIR.old"
|
||
mv "$STAGE/serverkit" "$INSTALL_DIR"
|
||
rm -rf "$STAGE"
|
||
good "Release deployed."
|
||
else
|
||
phase "Updating Source"
|
||
cd "$INSTALL_DIR"
|
||
|
||
chmod +x "$INSTALL_DIR/serverkit"
|
||
chmod +x "$INSTALL_DIR/scripts/"*.sh 2>/dev/null || true
|
||
|
||
step "Updating Python dependencies..."
|
||
source "$VENV_DIR/bin/activate"
|
||
pip install -r "$INSTALL_DIR/backend/requirements.txt" --quiet
|
||
|
||
step "Syncing app templates..."
|
||
mkdir -p /etc/serverkit/templates
|
||
cp -r "$INSTALL_DIR/backend/templates/"*.yaml /etc/serverkit/templates/ 2>/dev/null || true
|
||
cp -r "$INSTALL_DIR/backend/templates/"*.yml /etc/serverkit/templates/ 2>/dev/null || true
|
||
|
||
step "Rebuilding the frontend..."
|
||
cd "$INSTALL_DIR/frontend"
|
||
npm ci --prefer-offline 2>&1 | tail -3
|
||
NODE_OPTIONS="--max-old-space-size=1024" npm run build 2>&1 | tail -5
|
||
|
||
step "Rebuilding the frontend container..."
|
||
cd "$INSTALL_DIR"
|
||
docker compose build 2>&1 | tail -5
|
||
good "Source updated."
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Refresh nginx + systemd from the new tree
|
||
# ---------------------------------------------------------------------------
|
||
phase "Refreshing Configuration"
|
||
|
||
if [ -f "$INSTALL_DIR/nginx/sites-available/serverkit.conf" ]; then
|
||
cp "$INSTALL_DIR/nginx/sites-available/serverkit.conf" /etc/nginx/sites-available/
|
||
fi
|
||
|
||
mkdir -p /etc/nginx/serverkit-locations
|
||
for conf in /etc/nginx/sites-enabled/*; do
|
||
[ -f "$conf" ] || continue
|
||
name=$(basename "$conf")
|
||
case "$name" in serverkit-*) continue ;; esac
|
||
if grep -Eq 'server_name[[:space:]]+_;' "$conf" 2>/dev/null; then
|
||
warn "Removing conflicting catch-all config: $name"
|
||
rm -f "/etc/nginx/sites-enabled/$name" "/etc/nginx/sites-available/$name"
|
||
fi
|
||
done
|
||
|
||
if [ -f "$INSTALL_DIR/serverkit-backend.service" ]; then
|
||
cp "$INSTALL_DIR/serverkit-backend.service" /etc/systemd/system/serverkit.service
|
||
fi
|
||
systemctl daemon-reload
|
||
good "Configuration refreshed."
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Bring the stack back up
|
||
# ---------------------------------------------------------------------------
|
||
phase "Starting Services"
|
||
systemctl start "$BACKEND_SERVICE"
|
||
cd "$INSTALL_DIR"
|
||
docker compose up -d 2>&1 | tail -5
|
||
systemctl start nginx
|
||
good "Services started."
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Health check; restore the previous tree on failure
|
||
# ---------------------------------------------------------------------------
|
||
phase "Health Check"
|
||
|
||
roll_back() {
|
||
warn "Health check failed — rolling back..."
|
||
systemctl stop "$BACKEND_SERVICE" 2>/dev/null || true
|
||
systemctl stop nginx 2>/dev/null || true
|
||
if [ -d "$INSTALL_DIR.old" ]; then
|
||
rm -rf "$INSTALL_DIR"
|
||
mv "$INSTALL_DIR.old" "$INSTALL_DIR"
|
||
warn "Restored the previous installation from $INSTALL_DIR.old"
|
||
fi
|
||
systemctl daemon-reload
|
||
systemctl start "$BACKEND_SERVICE" 2>/dev/null || true
|
||
systemctl start nginx 2>/dev/null || true
|
||
halt "Update rolled back. Inspect logs: journalctl -u serverkit -n 50"
|
||
}
|
||
|
||
step "Waiting for the backend to answer..."
|
||
WAITED=0
|
||
while [ "$WAITED" -lt 30 ]; do
|
||
if curl -sf --max-time 5 http://127.0.0.1:5000/api/v1/system/health >/dev/null 2>&1; then
|
||
good "Backend healthy."
|
||
break
|
||
fi
|
||
sleep 2
|
||
WAITED=$((WAITED + 2))
|
||
done
|
||
[ "$WAITED" -ge 30 ] && roll_back
|
||
|
||
# Second probe as a belt-and-suspenders gate before we discard the old tree.
|
||
curl -sf --max-time 5 http://127.0.0.1:5000/api/v1/system/health >/dev/null 2>&1 || roll_back
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Cleanup + telemetry
|
||
# ---------------------------------------------------------------------------
|
||
phase "Cleanup"
|
||
|
||
[ -d "$INSTALL_DIR.old" ] && rm -rf "$INSTALL_DIR.old"
|
||
|
||
# Keep only the 10 most recent backups of each kind.
|
||
ls -t "$BACKUP_DIR"/serverkit-tree-* 2>/dev/null | tail -n +11 | xargs -r rm -rf
|
||
ls -t "$BACKUP_DIR"/serverkit-pre-upgrade-*.db 2>/dev/null | tail -n +11 | xargs -r rm -f
|
||
|
||
NEW_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null | tr -d '\n\r ' || echo "unknown")
|
||
curl -s "https://serverkit.ai/track/update?v=${NEW_VERSION}" >/dev/null 2>&1 || true
|
||
good "Cleanup complete."
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Summary
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n %s%s✔ Update complete%s %s%s%s\n\n' \
|
||
"$BLD" "$HUE_OK" "$RST" "$FOG" "$(clock)" "$RST"
|
||
printf ' Version %s\n' "$NEW_VERSION"
|
||
printf ' Backend %s\n' "$(systemctl is-active serverkit)"
|
||
printf ' Nginx %s\n\n' "$(systemctl is-active nginx)"
|
||
printf ' %sCLI%s serverkit status\n\n' "$BLD" "$RST"
|