From 32d211bca46e692421b0377dc1e249d64846a40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Fri, 8 May 2026 16:57:37 +0900 Subject: [PATCH] Add interactive VPS installer --- deploy/vps/install.sh | 410 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 deploy/vps/install.sh diff --git a/deploy/vps/install.sh b/deploy/vps/install.sh new file mode 100644 index 0000000..2f2dfb2 --- /dev/null +++ b/deploy/vps/install.sh @@ -0,0 +1,410 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# WSChat interactive VPS installer. +# FastPanel domains/sites must be created manually in FastPanel UI. + +PROJECT_NAME="WSChat" +DEFAULT_PROJECT_ROOT="/opt/ws-chat" +DEFAULT_REPO_URL="https://github.com/viktor138irk/chat.git" +DEFAULT_NODE_MAJOR="20" +DEFAULT_BACKEND_HOST="127.0.0.1" +DEFAULT_BACKEND_PORT="3000" +DEFAULT_PM2_PROCESS="wschat-backend" + +STEP=0 +TOTAL_STEPS=13 +LOG_FILE="/tmp/wschat-install-$(date +%Y%m%d-%H%M%S).log" + +PROJECT_ROOT="${PROJECT_ROOT:-$DEFAULT_PROJECT_ROOT}" +SOURCE_PATH="${SOURCE_PATH:-$PROJECT_ROOT/source}" +DATA_PATH="${DATA_PATH:-$PROJECT_ROOT/data}" +LOGS_PATH="${LOGS_PATH:-$PROJECT_ROOT/logs}" +BACKUPS_PATH="${BACKUPS_PATH:-$PROJECT_ROOT/backups}" +UPDATES_PATH="${UPDATES_PATH:-$PROJECT_ROOT/updates}" +REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" +NODE_MAJOR="${NODE_MAJOR:-$DEFAULT_NODE_MAJOR}" +BACKEND_HOST="${BACKEND_HOST:-$DEFAULT_BACKEND_HOST}" +BACKEND_PORT="${BACKEND_PORT:-$DEFAULT_BACKEND_PORT}" +PM2_PROCESS_NAME="${PM2_PROCESS_NAME:-$DEFAULT_PM2_PROCESS}" + +ADMIN_DOMAIN="${ADMIN_DOMAIN:-admin.example.ru}" +WIDGET_DOMAIN="${WIDGET_DOMAIN:-widget.example.ru}" +API_DOMAIN="${API_DOMAIN:-api.example.ru}" +ADMIN_WEBROOT="${ADMIN_WEBROOT:-}" +WIDGET_WEBROOT="${WIDGET_WEBROOT:-}" +TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" +JWT_SECRET="${JWT_SECRET:-}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +log() { + printf "%s\n" "$*" | tee -a "$LOG_FILE" +} + +info() { + printf "${BLUE}%s${NC}\n" "$*" | tee -a "$LOG_FILE" +} + +ok() { + printf "${GREEN}%s${NC}\n" "$*" | tee -a "$LOG_FILE" +} + +warn() { + printf "${YELLOW}%s${NC}\n" "$*" | tee -a "$LOG_FILE" +} + +fail() { + printf "${RED}%s${NC}\n" "$*" | tee -a "$LOG_FILE" >&2 +} + +current_step() { + STEP=$((STEP + 1)) + printf "\n${BOLD}${BLUE}[%02d/%02d] %s${NC}\n" "$STEP" "$TOTAL_STEPS" "$1" | tee -a "$LOG_FILE" +} + +run() { + log "→ $*" + "$@" 2>&1 | tee -a "$LOG_FILE" +} + +on_error() { + local exit_code=$? + fail "" + fail "Installation failed on step ${STEP}/${TOTAL_STEPS}. Exit code: ${exit_code}" + fail "Log file: ${LOG_FILE}" + fail "Last task should be visible above. Fix the error and rerun installer." + exit "$exit_code" +} +trap on_error ERR + +ask() { + local prompt="$1" + local default_value="$2" + local result + read -r -p "$(printf "${BOLD}%s${NC} [%s]: " "$prompt" "$default_value")" result + if [[ -z "$result" ]]; then + printf "%s" "$default_value" + else + printf "%s" "$result" + fi +} + +ask_secret() { + local prompt="$1" + local result + read -r -s -p "$(printf "${BOLD}%s${NC}: " "$prompt")" result + printf "\n" >&2 + printf "%s" "$result" +} + +generate_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 32 + else + date +%s%N | sha256sum | awk '{print $1}' + fi +} + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + fail "Run as root: bash deploy/vps/install.sh" + exit 1 + fi +} + +validate_fastpanel_path() { + local label="$1" + local path="$2" + if [[ -z "$path" ]]; then + warn "$label webroot is empty. Static publishing will be skipped." + return 0 + fi + if [[ "$path" != /var/www/*/data/www/* ]]; then + fail "$label webroot must look like /var/www//data/www/" + fail "Got: $path" + return 1 + fi +} + +write_backend_env() { + local env_file="$SOURCE_PATH/backend/.env" + cat > "$env_file" < "$PROJECT_ROOT/install-state.env" </dev/null 2>&1 && node -v | grep -q "^v${NODE_MAJOR}\."; then + ok "Node.js already installed: $(node -v)" + return 0 + fi + run bash -c "curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash -" + run apt install -y nodejs + ok "Node.js: $(node -v)" + ok "npm: $(npm -v)" +} + +install_pm2() { + if command -v pm2 >/dev/null 2>&1; then + ok "PM2 already installed: $(pm2 -v)" + else + run npm install -g pm2 + ok "PM2 installed: $(pm2 -v)" + fi +} + +clone_or_update_repo() { + mkdir -p "$PROJECT_ROOT" + if [[ -d "$SOURCE_PATH/.git" ]]; then + cd "$SOURCE_PATH" + run git fetch origin main + run git pull --ff-only origin main + else + mkdir -p "$(dirname "$SOURCE_PATH")" + run git clone "$REPO_URL" "$SOURCE_PATH" + fi +} + +install_dependencies() { + cd "$SOURCE_PATH" + run npm install +} + +start_backend_pm2() { + cd "$SOURCE_PATH" + if pm2 describe "$PM2_PROCESS_NAME" >/dev/null 2>&1; then + run pm2 restart "$PM2_PROCESS_NAME" --update-env + else + run pm2 start backend/src/server.js --name "$PM2_PROCESS_NAME" + fi + run pm2 save + pm2 startup systemd -u root --hp /root 2>&1 | tee -a "$LOG_FILE" || true +} + +check_backend() { + sleep 2 + run curl -fsS "http://${BACKEND_HOST}:${BACKEND_PORT}/health" +} + +build_frontend() { + cd "$SOURCE_PATH" + run npm run build:admin + run npm run build:widget +} + +publish_static_if_configured() { + cd "$SOURCE_PATH" + if [[ -n "$ADMIN_WEBROOT" ]]; then + if [[ ! -d "$ADMIN_WEBROOT" ]]; then + warn "Admin webroot does not exist: $ADMIN_WEBROOT" + warn "Skipping admin publish. Create the site manually in FastPanel first." + else + run rsync -av --delete admin-panel/dist/ "$ADMIN_WEBROOT/" + fi + fi + + if [[ -n "$WIDGET_WEBROOT" ]]; then + if [[ ! -d "$WIDGET_WEBROOT" ]]; then + warn "Widget webroot does not exist: $WIDGET_WEBROOT" + warn "Skipping widget publish. Create the site manually in FastPanel first." + else + run rsync -av --delete widget/dist/ "$WIDGET_WEBROOT/" + fi + fi +} + +main() { + print_header + require_root + + current_step "Collect configuration" + PROJECT_ROOT="$(ask "Project directory" "$PROJECT_ROOT")" + SOURCE_PATH="${PROJECT_ROOT}/source" + DATA_PATH="${PROJECT_ROOT}/data" + LOGS_PATH="${PROJECT_ROOT}/logs" + BACKUPS_PATH="${PROJECT_ROOT}/backups" + UPDATES_PATH="${PROJECT_ROOT}/updates" + + ADMIN_DOMAIN="$(ask "Admin domain" "$ADMIN_DOMAIN")" + WIDGET_DOMAIN="$(ask "Widget domain" "$WIDGET_DOMAIN")" + API_DOMAIN="$(ask "API domain" "$API_DOMAIN")" + ADMIN_WEBROOT="$(ask "Admin FastPanel webroot, leave empty to skip publishing" "$ADMIN_WEBROOT")" + WIDGET_WEBROOT="$(ask "Widget FastPanel webroot, leave empty to skip publishing" "$WIDGET_WEBROOT")" + TELEGRAM_BOT_TOKEN="$(ask_secret "Telegram bot token, leave empty for now")" + JWT_SECRET="$(ask "JWT secret" "$(generate_secret)")" + + validate_fastpanel_path "Admin" "$ADMIN_WEBROOT" + validate_fastpanel_path "Widget" "$WIDGET_WEBROOT" + print_summary + read -r -p "Continue installation? [Y/n]: " confirm + if [[ "$confirm" =~ ^[Nn]$ ]]; then + warn "Installation cancelled." + exit 0 + fi + + current_step "Install base packages" + install_packages + + current_step "Install Node.js" + install_node + + current_step "Install PM2" + install_pm2 + + current_step "Create WSChat directories" + mkdir -p "$PROJECT_ROOT" "$DATA_PATH" "$LOGS_PATH" "$BACKUPS_PATH" "$UPDATES_PATH" + ok "Directories created under $PROJECT_ROOT" + + current_step "Clone or update repository" + clone_or_update_repo + + current_step "Install project dependencies" + install_dependencies + + current_step "Write backend .env" + write_backend_env + write_install_state + ok "Backend env written to $SOURCE_PATH/backend/.env" + ok "Install state written to $PROJECT_ROOT/install-state.env" + + current_step "Start backend with PM2" + start_backend_pm2 + + current_step "Check backend health" + check_backend + + current_step "Build admin and widget" + build_frontend + + current_step "Publish static files if webroots are configured" + publish_static_if_configured + + current_step "Print FastPanel proxy instructions" + cat <