#!/usr/bin/env bash # # Certain Finance – nativer Installer für einen Debian/Ubuntu-LXC (Proxmox). # Richtet Python-venv, gebautes Frontend, systemd-Dienst und Konfiguration ein. # HTTPS macht dein vorhandener Nginx Proxy Manager – hier wird KEIN Reverse-Proxy # installiert; der Dienst lauscht nur intern auf 127.0.0.1:. # # Nutzung (als root im geklonten Repo): # ./deploy/install.sh interaktiv (fragt Port/Admin) # ./deploy/install.sh --yes nicht-interaktiv (Zufalls-Admin-Passwort) # set -euo pipefail APP_SLUG="certain-finance" # Einzige Stelle mit dem Betriebs-Slug (Dienst/Pfade) DEFAULT_PORT=8071 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ETC_DIR="/etc/$APP_SLUG" ENV_FILE="$ETC_DIR/env" DATA_DIR="/var/lib/$APP_SLUG" # Der Dienst läuft als EIGENTÜMER des Repos. Wer als root nach /root klont, für den # läuft der Dienst als root – nur so ist /root/... für den Dienst überhaupt # zugänglich (ein eigener Dienst-Benutzer käme sonst nicht an Code/venv unter /root). SERVICE_USER="$(stat -c '%U' "$REPO_DIR" 2>/dev/null || echo root)" SERVICE_GROUP="$(stat -c '%G' "$REPO_DIR" 2>/dev/null || echo root)" UNIT_FILE="/etc/systemd/system/$APP_SLUG.service" ASSUME_YES=0 [[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && ASSUME_YES=1 info() { printf '\033[0;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*"; } error() { printf '\033[0;31m[x]\033[0m %s\n' "$*" >&2; } rand() { LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null 2>&1; then error "Dieses Skript ist für Debian/Ubuntu (apt) gedacht. Kein apt-get gefunden." exit 1 fi # --- Systempakete ------------------------------------------------------------ info "Installiere Systempakete (Python, Build-Tools, git, curl) …" export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get install -y python3 python3-venv python3-dev build-essential curl ca-certificates git \ tesseract-ocr tesseract-ocr-deu # --- Python-Version prüfen (3.11+ erforderlich) ------------------------------ PY_VER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || echo 0.0)" PY_MAJOR="${PY_VER%%.*}" PY_MINOR="${PY_VER##*.}" if [[ "$PY_MAJOR" -lt 3 || ( "$PY_MAJOR" -eq 3 && "$PY_MINOR" -lt 11 ) ]]; then error "Python 3.11+ wird benötigt – gefunden: $PY_VER." error "Bitte einen Debian-12- oder Ubuntu-24.04-LXC verwenden (dort ist Python 3.11+ dabei)." error "Ältere Systeme (z. B. Debian 11 = Python 3.9) werden nicht unterstützt." exit 1 fi info "Python $PY_VER gefunden." # --- Node.js (nur zum Bauen des Frontends nötig) ----------------------------- ensure_node() { local major=0 if command -v node >/dev/null 2>&1; then major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" fi if [[ "$major" -ge 18 ]]; then info "Node.js gefunden: $(node --version)" return fi warn "Node.js fehlt oder ist zu alt – installiere Node 20 LTS (NodeSource) …" curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs info "Node.js installiert: $(node --version)" } ensure_node # --- Verzeichnisse (Dienst läuft als Repo-Eigentümer, s. o.) ---------------- info "Dienst läuft als Benutzer '$SERVICE_USER' (Eigentümer des Repos)." mkdir -p "$DATA_DIR" "$ETC_DIR" chown -R "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR" # --- Python-venv + Abhängigkeiten ------------------------------------------- info "Richte Python-venv ein und installiere Abhängigkeiten …" python3 -m venv "$REPO_DIR/backend/.venv" "$REPO_DIR/backend/.venv/bin/pip" install --upgrade pip >/dev/null "$REPO_DIR/backend/.venv/bin/pip" install -r "$REPO_DIR/backend/requirements.txt" if [[ ! -x "$REPO_DIR/backend/.venv/bin/uvicorn" ]]; then error "uvicorn wurde nicht in die venv installiert – die Abhängigkeiten sind unvollständig." error "Bitte die Ausgabe oben auf pip-Fehler prüfen." exit 1 fi # --- Frontend bauen ---------------------------------------------------------- info "Baue das Frontend (Node wird nur hier gebraucht) …" ( cd "$REPO_DIR/web" if [[ -f package-lock.json ]]; then npm ci; else npm install; fi npm run build ) # --- Konfiguration (.env) ---------------------------------------------------- GIT_SHA="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo '')" if [[ -f "$ENV_FILE" ]]; then info "Konfiguration existiert bereits: $ENV_FILE (unverändert übernommen)." # Git-Commit aktualisieren (für /api/version). if grep -q '^APP_GIT_SHA=' "$ENV_FILE"; then sed -i "s|^APP_GIT_SHA=.*|APP_GIT_SHA=$GIT_SHA|" "$ENV_FILE" else echo "APP_GIT_SHA=$GIT_SHA" >> "$ENV_FILE" fi # Direktzugriff (http://IP:Port) für ältere Konfigurationen nachrüsten. grep -q '^APP_BIND_HOST=' "$ENV_FILE" || echo "APP_BIND_HOST=0.0.0.0" >> "$ENV_FILE" ADMIN_USER="$(grep -E '^APP_ADMIN_USERNAME=' "$ENV_FILE" | cut -d= -f2-)" GENERATED_PASS="" else local_port="$DEFAULT_PORT" admin_user="admin" admin_pass="$(rand 16)" if [[ "$ASSUME_YES" -eq 0 ]]; then read -r -p "Interner Port [$DEFAULT_PORT]: " x; local_port="${x:-$DEFAULT_PORT}" read -r -p "Admin-Benutzername [admin]: " x; admin_user="${x:-admin}" read -r -s -p "Admin-Passwort [leer = zufällig]: " x; echo [[ -n "$x" ]] && admin_pass="$x" fi info "Erzeuge Konfiguration: $ENV_FILE" cat > "$ENV_FILE" < "$UNIT_FILE" < "Failed to locate executable .../uvicorn". ReadWritePaths=$DATA_DIR [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable "$APP_SLUG" >/dev/null 2>&1 || true info "Starte Dienst … (wendet Migrationen an und legt ggf. den ersten Admin an)" systemctl restart "$APP_SLUG" # --- Gesundheitscheck -------------------------------------------------------- PORT="$(grep -E '^APP_PORT=' "$ENV_FILE" | cut -d= -f2-)" PORT="${PORT:-$DEFAULT_PORT}" info "Warte auf das Backend (127.0.0.1:$PORT) …" ok=0 for _ in $(seq 1 30); do if curl -fsS "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then ok=1; break; fi sleep 2 done LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" echo if [[ "$ok" -eq 1 ]]; then info "Fertig – Certain Finance läuft. 🎉" else warn "Backend antwortet noch nicht. Logs: journalctl -u $APP_SLUG -e" fi echo " Web-UI (direkt): http://${LAN_IP:-}:$PORT" echo " Admin-Login: ${ADMIN_USER:-admin}" if [[ -n "${GENERATED_PASS:-}" ]]; then echo " Admin-Passwort: ${GENERATED_PASS}" warn "Bitte dieses Passwort notieren – es wird nicht erneut angezeigt." fi echo echo " Optional über HTTPS (Nginx Proxy Manager): neuen Proxy-Host ->" echo " Forward Hostname/IP: ${LAN_IP:-}" echo " Forward Port: $PORT (Scheme: http)" echo " SSL: Let's Encrypt aktivieren. Dann in $ENV_FILE APP_COOKIE_SECURE=true" echo " setzen und 'systemctl restart $APP_SLUG'. (Uploads: client_max_body_size >= 20M.)" echo echo " Logs: journalctl -u $APP_SLUG -f" echo " Neustart: systemctl restart $APP_SLUG" echo " Update: ./deploy/update.sh"