From 225bb491a6c4dc8219c6e7acc7622bc53281faf1 Mon Sep 17 00:00:00 2001 From: Renato Date: Tue, 28 Jul 2026 00:24:16 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20direct=20VPS=20installer=20=E2=80=94=20?= =?UTF-8?q?Node.js=2022=20+=20Caddy=20+=20systemd,=20no=20Docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Docker-based install.sh with a direct-install approach: - Docker cleanup: removes container, stops daemon, uninstalls packages - Node.js 22 via NodeSource apt/rpm/pacman repos - Caddy via Cloudsmith apt/COPR/pacman repos - Clone, npm ci, npm run build, standalone output - systemd unit (worst-scan-web.service) — user-level service, HOSTNAME=127.0.0.1 - Caddy reverse proxy on :80 → 127.0.0.1:3000 - deploy/ reference files for manual editing ~150-160 MB RAM total (vs ~300-400 MB with Docker) on 1GB VPS --- deploy/Caddyfile | 7 + deploy/worst-scan-web.service | 19 ++ install.sh | 314 ++++++++++++++++++---------------- 3 files changed, 192 insertions(+), 148 deletions(-) create mode 100644 deploy/Caddyfile create mode 100644 deploy/worst-scan-web.service diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..2a2f26a --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,7 @@ +# worst-scan-web — Caddy reverse proxy +# Copy to /etc/caddy/Caddyfile or include from there. +# Replace :80 with your domain for auto-HTTPS. + +:80 { + reverse_proxy 127.0.0.1:3000 +} diff --git a/deploy/worst-scan-web.service b/deploy/worst-scan-web.service new file mode 100644 index 0000000..6a34c68 --- /dev/null +++ b/deploy/worst-scan-web.service @@ -0,0 +1,19 @@ +[Unit] +Description=worst-scan-web +After=network.target + +[Service] +Type=exec +User=ren +WorkingDirectory=/home/ren/worst-scan-web/.next/standalone +Environment=NODE_ENV=production +Environment=PORT=3000 +Environment=HOSTNAME=127.0.0.1 +Environment=DB_PATH=/home/ren/worst-scan-web/data/worst-scan.db +Environment=COVERS_DIR=/home/ren/worst-scan-web/data/covers +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/install.sh b/install.sh index a087739..d61e888 100755 --- a/install.sh +++ b/install.sh @@ -2,167 +2,113 @@ set -euo pipefail # ────────────────────────────────────────────── -# worst-scan-web — Docker auto-installer +# worst-scan-web — Direct installer (no Docker) # Usage: bash <(curl -fsSL https://gitea.cbcren.online/renato97/worst-scan-web/raw/branch/main/install.sh) -# Or: curl -fsSL https://gitea.cbcren.online/renato97/worst-scan-web/raw/branch/main/install.sh -o install.sh && bash install.sh # ────────────────────────────────────────────── REPO_URL="https://gitea.cbcren.online/renato97/worst-scan-web.git" INSTALL_DIR="${HOME}/worst-scan-web" BRANCH="main" -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' log() { printf "${GREEN}✓${NC} %s\n" "$1"; } warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; } err() { printf "${RED}✗${NC} %s\n" "$1"; } info() { printf "${CYAN}→${NC} %s\n" "$1"; } -# ── OS detection ────────────────────────────────── +# ── OS detection ──────────────────────────────── detect_os() { if [ -f /etc/os-release ]; then - . /etc/os-release - echo "$ID" + . /etc/os-release && echo "$ID" elif command -v lsb_release &>/dev/null; then lsb_release -si | tr '[:upper:]' '[:lower:]' - else - echo "unknown" - fi + else echo "unknown"; fi } - OS=$(detect_os) -RAW_ARCH=$(uname -m) -case "$RAW_ARCH" in - x86_64) ARCH="amd64" ;; - aarch64) ARCH="arm64" ;; - *) ARCH="$RAW_ARCH" ;; -esac -# ── Git ────────────────────────────────────────── -if ! command -v git &>/dev/null; then - info "Installing git..." - case "$OS" in - ubuntu|debian) sudo apt update -qq && sudo apt install -y -qq git ;; - arch|endeavouros) sudo pacman -S --noconfirm git ;; - fedora|centos|rhel|almalinux|rocky) sudo dnf install -y git ;; - *) err "Unsupported OS: $OS. Install git manually." && exit 1 ;; - esac - log "git installed" +bail() { err "Unsupported OS: $OS. Install manually: $1"; exit 1; } + +# ── Sudo check ───────────────────────────────── +sudo -v || (err "sudo required" && exit 1) + +# ── git + curl ───────────────────────────────── +for cmd in git curl; do + if ! command -v "$cmd" &>/dev/null; then + info "Installing $cmd..." + case "$OS" in + ubuntu|debian) sudo apt update -qq && sudo apt install -y -qq "$cmd" ;; + arch|endeavouros) sudo pacman -S --noconfirm "$cmd" ;; + fedora|centos|rhel|*) sudo dnf install -y "$cmd" ;; + esac + log "$cmd installed" + fi +done + +# ── Docker cleanup ────────────────────────────── +if command -v docker &>/dev/null; then + info "Cleaning up Docker..." + if [ -f "$INSTALL_DIR/docker-compose.yml" ]; then + (cd "$INSTALL_DIR" && docker compose down) 2>/dev/null || true + fi + sudo docker stop worst-scan-web 2>/dev/null || true + sudo docker rm worst-scan-web 2>/dev/null || true + sudo systemctl stop docker docker.socket 2>/dev/null || true + sudo systemctl disable docker docker.socket 2>/dev/null || true + sudo apt remove -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras 2>/dev/null || true + sudo rm -f /etc/apt/sources.list.d/docker.list /etc/apt/keyrings/docker.asc + sudo apt update -qq 2>/dev/null || true + log "Docker removed" fi -# ── Docker ─────────────────────────────────────── -NEED_DOCKER=false -NEED_COMPOSE=false - -if ! command -v docker &>/dev/null; then - NEED_DOCKER=true -fi - -if ! docker compose version &>/dev/null && ! docker-compose --version &>/dev/null; then - NEED_COMPOSE=true -fi - -if $NEED_DOCKER; then - info "Installing Docker Engine..." +# ── Node.js 22 ────────────────────────────────── +if ! command -v node &>/dev/null || [ "$(node -v | cut -d. -f1 | tr -d v)" -lt 22 ]; then + info "Installing Node.js 22..." case "$OS" in ubuntu|debian) - curl -fsSL https://get.docker.com | sh + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash + sudo apt install -y -qq nodejs ;; arch|endeavouros) - sudo pacman -S --noconfirm docker + sudo pacman -S --noconfirm nodejs npm ;; fedora) - sudo dnf install -y dnf-plugins-core - sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo - sudo dnf install -y docker-ce docker-ce-cli containerd.io - ;; - centos|rhel|almalinux|rocky) - sudo dnf install -y dnf-plugins-core - sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo - sudo dnf install -y docker-ce docker-ce-cli containerd.io - ;; - *) - err "Unsupported OS: $OS. Install Docker manually: https://docs.docker.com/engine/install/" - exit 1 + curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash + sudo dnf install -y nodejs ;; + *) bail "https://nodejs.org/en/download/" ;; esac - log "Docker Engine installed" + log "Node.js $(node -v) installed" +else + log "Node.js $(node -v) already installed" fi -# Re-check compose — fresh Docker install may have included it -if $NEED_COMPOSE && docker compose version &>/dev/null; then - NEED_COMPOSE=false -fi - -if $NEED_COMPOSE; then - info "Installing Docker Compose plugin..." +# ── Caddy ──────────────────────────────────────── +if ! command -v caddy &>/dev/null; then + info "Installing Caddy..." case "$OS" in ubuntu|debian) - sudo apt install -y -qq docker-compose-v2 2>/dev/null || { - sudo apt install -y -qq docker-compose-plugin 2>/dev/null || { - warn "Compose plugin not in apt. Installing via CLI plugin..." - CLI_PLUGINS="/usr/lib/docker/cli-plugins" - sudo mkdir -p "$CLI_PLUGINS" - sudo curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o "$CLI_PLUGINS/docker-compose" - sudo chmod +x "$CLI_PLUGINS/docker-compose" - } - } + sudo apt install -y -qq debian-keyring debian-archive-keyring apt-transport-https + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/setup.deb.sh' | sudo bash + sudo apt install -y -qq caddy ;; arch|endeavouros) - sudo pacman -S --noconfirm docker-compose + sudo pacman -S --noconfirm caddy ;; - fedora|centos|rhel|almalinux|rocky) - sudo dnf install -y docker-compose-v2 2>/dev/null || { - CLI_PLUGINS="/usr/lib/docker/cli-plugins" - sudo mkdir -p "$CLI_PLUGINS" - sudo curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o "$CLI_PLUGINS/docker-compose" - sudo chmod +x "$CLI_PLUGINS/docker-compose" - } + fedora) + sudo dnf install -y 'dnf-command(copr)' + sudo dnf copr enable -y @caddy/caddy + sudo dnf install -y caddy ;; + *) bail "https://caddyserver.com/docs/install" ;; esac - log "Docker Compose plugin installed" -fi - -log "Docker prerequisites satisfied" - -# ── Start Docker daemon ───────────────────────── -if ! docker info &>/dev/null; then - info "Starting Docker daemon..." - if command -v systemctl &>/dev/null; then - sudo systemctl enable --now docker - elif command -v service &>/dev/null; then - sudo service docker start - else - sudo dockerd &>/dev/null & - sleep 3 - fi -fi -log "Docker daemon is running" - -# ── Add user to docker group ──────────────────── -if ! groups "$USER" | grep -q docker; then - info "Adding $USER to the docker group..." - sudo usermod -aG docker "$USER" - # Spawn subshell with the new group so rest of script can use docker without sudo - sg docker -c "true" 2>/dev/null || true - warn "You may need to re-login for group changes to take effect in new shells" -fi - -# Use sudo for docker commands if the current shell can't run docker directly -if docker info &>/dev/null 2>&1; then - docker_cmd() { docker "$@"; } + log "Caddy $(caddy version | cut -d' ' -f1) installed" else - docker_cmd() { sudo docker "$@"; } + log "Caddy already installed" fi # ── Clone / Pull ───────────────────────────────── if [ -d "$INSTALL_DIR" ]; then - info "Directory $INSTALL_DIR already exists — pulling latest" + info "Updating repository..." cd "$INSTALL_DIR" git fetch origin "$BRANCH" git reset --hard "origin/$BRANCH" @@ -179,60 +125,132 @@ INSTALL_DIR="$(cd "$INSTALL_DIR" && pwd)" # ── .env ───────────────────────────────────────── if [ -f "$INSTALL_DIR/.env" ]; then warn ".env already exists — not overwriting" - info "Edit $INSTALL_DIR/.env if you need to change settings" else cp "$INSTALL_DIR/.env.example" "$INSTALL_DIR/.env" log "Created .env from .env.example" - info "Open $INSTALL_DIR/.env and set up your API_BASE_URL, API_KEY, WEB_PASSWORD" - info "Then run the web setup wizard at http://YOUR_IP:3000/setup after starting" fi # ── Data directory ──────────────────────────────── mkdir -p "$INSTALL_DIR/data" log "Data directory ready" -# ── Build & Start ──────────────────────────────── -info "Building Docker image (this may take a few minutes)..." -docker_cmd compose build web +# ── Build ──────────────────────────────────────── +info "Installing npm dependencies..." +cd "$INSTALL_DIR" +npm ci +log "Dependencies installed" + +info "Building Next.js app (this may take a few minutes)..." +npm run build log "Build complete" -info "Starting containers..." -docker_cmd compose up -d -log "Containers are running" +STANDALONE_DIR="$INSTALL_DIR/.next/standalone" + +# Ensure static files are copied +if [ ! -d "$STANDALONE_DIR/.next/static" ]; then + mkdir -p "$STANDALONE_DIR/.next" + cp -r "$INSTALL_DIR/.next/static" "$STANDALONE_DIR/.next/static" +fi +cp -r "$INSTALL_DIR/public" "$STANDALONE_DIR/public" 2>/dev/null || true + +# ── systemd service ────────────────────────────── +SERVICE_NAME="worst-scan-web" +APP_USER="$USER" +APP_HOME="$HOME" + +sudo tee "/etc/systemd/system/$SERVICE_NAME.service" > /dev/null </dev/null; then + warn "Caddy config already contains worst-scan-web block — not modifying" +else + sudo mkdir -p "$(dirname "$CADDYFILE")" + + if [ -s "$CADDYFILE" ] && [ "$(wc -l < "$CADDYFILE")" -gt 2 ]; then + EXISTING_IMPORT=$(grep -c 'import' "$CADDYFILE" 2>/dev/null || true) + if [ "$EXISTING_IMPORT" -eq 0 ]; then + sudo tee -a "$CADDYFILE" > /dev/null < /dev/null </dev/null || true +sudo systemctl restart caddy +log "Caddy configured" # ── Wait for health ────────────────────────────── -info "Waiting for web service to be healthy..." -for i in $(seq 1 30); do - if docker_cmd compose exec -T web wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health &>/dev/null; then - log "Web service is healthy" - break - fi - if [ "$i" -eq 30 ]; then - warn "Timed out waiting for health check — check logs with: docker compose logs web" +info "Waiting for web service..." +for i in $(seq 1 15); do + if curl -fsS http://127.0.0.1:3000/api/health >/dev/null 2>&1; then + log "Web service is healthy"; break fi + if [ "$i" -eq 15 ]; then warn "Timed out — check: journalctl -u $SERVICE_NAME -f"; fi sleep 2 done -# ── Get IP ────────────────────────────────────── +# ── IP ────────────────────────────────────────── IP=$(curl -fsSL http://checkip.amazonaws.com 2>/dev/null || curl -fsSL https://api.ipify.org 2>/dev/null || echo "localhost") # ── Summary ────────────────────────────────────── -BOX_W=60 +BOX_W=58 hr() { printf "║ %-*s ║\n" "$((BOX_W-6))" "$1"; } printf "\n" printf "╔══════════════════════════════════════════════════════════════╗\n" -printf "║ ${GREEN}worst-scan-web installed!${NC} ║\n" +printf "║ ${GREEN}worst-scan-web installed!${NC} ║\n" printf "╠══════════════════════════════════════════════════════════════╣\n" -hr "Web: http://$IP:3000" +hr "Web: http://$IP" +hr "Setup: http://$IP/setup" hr "Config: $INSTALL_DIR/.env" -hr "Logs: cd $INSTALL_DIR && docker compose logs -f" -hr "Restart: cd $INSTALL_DIR && docker compose restart" -hr "Stop: cd $INSTALL_DIR && docker compose down" -hr "Update: bash install.sh" +hr "Repo: $INSTALL_DIR" +hr "" +hr "Manage: sudo systemctl restart $SERVICE_NAME" +hr "Logs: journalctl -u $SERVICE_NAME -f" +hr "Update: cd $INSTALL_DIR && git pull && npm run build" +hr " && sudo systemctl restart $SERVICE_NAME" +hr "" +hr "Caddy: sudo systemctl restart caddy" +hr "Cfg: $CADDYFILE" printf "╚══════════════════════════════════════════════════════════════╝\n" printf "\n" -info "Next steps:" -printf " 1. Open http://%s:3000/setup in your browser\n" "$IP" -printf " 2. Complete the web setup wizard\n" -printf " 3. Log in and start using worst-scan-web\n" -printf "\n"