#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # RAPPORT Server — Proxmox-LXC-Installer # # Läuft auf der PROXMOX-VE-HOST-SHELL (nicht im Container!). # Baut einen unprivilegierten Debian-12-LXC, installiert Docker, klont den # Rapport-Compose-Stack (SERVER-CONTAINER), generiert Secrets + JWT-Keys, # holt die DB-Migrations, setzt die LAN-URLs und startet den Stack. # # bash -c "$(curl -fsSL http://git.kgva.ch/karim/RAPPORT-SERVER-PROXMOX-LXC/raw/branch/main/rapport-lxc.sh)" # # Auf einem echten Terminal erscheint ein whiptail-Menü (Standard/Erweitert). # Per SSH/Pipe (z.B. deploy-local.sh) läuft es non-interaktiv auf Env-Vars: # RAM_MB=8192 NET_IP=192.168.1.50/24 NET_GW=192.168.1.1 bash rapport-lxc.sh # Menü erzwingen-aus: NONINTERACTIVE=1 # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail VERSION="0.1.0" # ═══ Konfiguration ══════════════════════════════════════════════════════════ CTID="${CTID:-$(pvesh get /cluster/nextid)}" # freie Container-ID CT_HOSTNAME="${CT_HOSTNAME:-rapport-server}" DISK_GB="${DISK_GB:-20}" # Supabase-Images sind gross CORES="${CORES:-2}" RAM_MB="${RAM_MB:-6144}" # Postgres + 6 Services, min. 4 GB SWAP_MB="${SWAP_MB:-2048}" BRIDGE="${BRIDGE:-vmbr0}" STORAGE="${STORAGE:-local-lvm}" # Storage für rootfs TMPL_STORAGE="${TMPL_STORAGE:-local}" # Storage für Template-Cache NET_IP="${NET_IP:-dhcp}" # dhcp ODER z.B. 192.168.1.50/24 NET_GW="${NET_GW:-}" # nur bei statischer IP nötig PASSWORD="${PASSWORD:-}" # root-PW im Container (leer = kein Login) REPO_URL="${REPO_URL:-http://git.kgva.ch/karim/RAPPORT-SERVER.git}" REPO_REF="${REPO_REF:-main}" TEMPLATE="${TEMPLATE:-debian-12-standard}" # pveam-Template-Name (Präfix) NONINTERACTIVE="${NONINTERACTIVE:-0}" # 1 = whiptail-Dialoge überspringen # ── Local-Modus (kein Gitea nötig) ────────────────────────────────────────── # Wenn gesetzt, werden diese Tarballs (Pfade AUF DEM PROXMOX-HOST) per # `pct push` in den Container geschoben statt von Gitea geklont/gesynct. # Setzt deploy-local.sh automatisch — siehe README. LOCAL_STACK="${LOCAL_STACK:-}" # tar.gz des SERVER-CONTAINER-Working-Copy LOCAL_MIGRATIONS="${LOCAL_MIGRATIONS:-}" # tar.gz mit *.sql (APP/supabase/migrations) # ═══ Ausgabe-Helfer ═════════════════════════════════════════════════════════ GN='\033[0;32m'; YW='\033[0;33m'; RD='\033[0;31m'; BL='\033[1;34m'; CL='\033[0m' msg() { echo -e "${GN}✔${CL} $*"; } info() { echo -e "${YW}→${CL} $*"; } die() { echo -e "${RD}✗${CL} $*" >&2; exit 1; } echo -e "${BL}RAPPORT Server — Proxmox-LXC-Installer v${VERSION}${CL}" # ═══ Vorbedingungen ═════════════════════════════════════════════════════════ [[ $EUID -eq 0 ]] || die "Bitte als root auf der Proxmox-Host-Shell ausführen." command -v pct >/dev/null || die "pct nicht gefunden — läuft das auf einem Proxmox-VE-Host?" command -v pveam >/dev/null || die "pveam nicht gefunden — Proxmox-VE-Host erwartet." # ═══ Interaktive Einstellungen (whiptail) ═══════════════════════════════════ # Läuft nur auf einem echten Terminal (also beim curl-Einzeiler in der # Proxmox-Shell). Über SSH/Pipe (z.B. deploy-local.sh) fällt es automatisch # auf die Env-Var-Defaults zurück. whip() { whiptail --backtitle "RAPPORT Server — Proxmox-LXC v${VERSION}" "$@" 3>&1 1>&2 2>&3; } gather_settings() { command -v whiptail >/dev/null || return 0 [[ -t 0 && "$NONINTERACTIVE" != "1" ]] || { info "Nicht-interaktiv — verwende Env-Vars/Defaults."; return 0; } local mode mode=$(whip --title "Installationsmodus" --menu "\nWie möchtest du den Rapport-Server-Container anlegen?" 15 64 3 \ "standard" "Standard-Einstellungen (empfohlen)" \ "erweitert" "Erweitert — alle Werte anpassen" \ "abbrechen" "Abbrechen") || die "Abgebrochen." [[ "$mode" == "abbrechen" ]] && die "Abgebrochen." if [[ "$mode" == "erweitert" ]]; then CTID=$(whip --title "Container-ID" --inputbox "Eindeutige LXC-ID" 8 60 "$CTID") || die "Abgebrochen." CT_HOSTNAME=$(whip --title "Hostname" --inputbox "Hostname des Containers" 8 60 "$CT_HOSTNAME") || die "Abgebrochen." CORES=$(whip --title "CPU" --inputbox "Anzahl CPU-Kerne" 8 60 "$CORES") || die "Abgebrochen." RAM_MB=$(whip --title "RAM" --inputbox "Arbeitsspeicher in MB (min. 4096)" 8 60 "$RAM_MB") || die "Abgebrochen." DISK_GB=$(whip --title "Disk" --inputbox "Festplatte in GB (min. 16)" 8 60 "$DISK_GB") || die "Abgebrochen." STORAGE=$(whip --title "Storage" --inputbox "Proxmox-Storage für rootfs" 8 60 "$STORAGE") || die "Abgebrochen." BRIDGE=$(whip --title "Netzwerk-Bridge" --inputbox "Bridge" 8 60 "$BRIDGE") || die "Abgebrochen." local netmode netmode=$(whip --title "Netzwerk" --menu "\nIP-Adresse" 12 60 2 \ "dhcp" "Automatisch (DHCP)" \ "static" "Statische IP") || die "Abgebrochen." if [[ "$netmode" == "static" ]]; then NET_IP=$(whip --title "Statische IP" --inputbox "IP in CIDR-Notation, z.B. 192.168.1.50/24" 8 64 "${NET_IP/dhcp/192.168.1.50/24}") || die "Abgebrochen." NET_GW=$(whip --title "Gateway" --inputbox "Gateway-IP, z.B. 192.168.1.1" 8 64 "$NET_GW") || die "Abgebrochen." else NET_IP="dhcp" fi PASSWORD=$(whip --title "Root-Passwort" --passwordbox "Root-Passwort im Container (leer = kein Login)" 8 64 "") || die "Abgebrochen." fi # Zusammenfassung + Bestätigung whip --title "Bitte bestätigen" --yesno "\ Container anlegen mit: ID: $CTID Hostname: $CT_HOSTNAME CPU/RAM: ${CORES} Kerne / ${RAM_MB} MB Disk: ${DISK_GB} GB auf ${STORAGE} Netzwerk: ${NET_IP}$([[ "$NET_IP" != dhcp ]] && echo " gw ${NET_GW}") @ ${BRIDGE} Jetzt starten?" 17 64 || die "Abgebrochen." } gather_settings # ═══ 1 · Debian-Template sicherstellen ══════════════════════════════════════ info "Suche Debian-12-Template …" pveam update >/dev/null 2>&1 || true TMPL_FILE="$(pveam available --section system | awk -v t="$TEMPLATE" '$2 ~ t {print $2}' | sort -V | tail -n1)" [[ -n "$TMPL_FILE" ]] || die "Kein Template '$TEMPLATE*' verfügbar (siehe: pveam available)." if ! pveam list "$TMPL_STORAGE" 2>/dev/null | grep -q "$TMPL_FILE"; then info "Lade Template $TMPL_FILE auf $TMPL_STORAGE …" pveam download "$TMPL_STORAGE" "$TMPL_FILE" fi TMPL_REF="${TMPL_STORAGE}:vztmpl/${TMPL_FILE}" msg "Template: $TMPL_REF" # ═══ 2 · Netzwerk-String bauen ══════════════════════════════════════════════ if [[ "$NET_IP" == "dhcp" ]]; then NETCFG="name=eth0,bridge=${BRIDGE},ip=dhcp" else [[ -n "$NET_GW" ]] || die "Bei statischer IP (NET_IP=$NET_IP) muss NET_GW gesetzt sein." NETCFG="name=eth0,bridge=${BRIDGE},ip=${NET_IP},gw=${NET_GW}" fi # ═══ 3 · Container erstellen (unprivilegiert + Docker-Features) ══════════════ info "Erstelle LXC #${CTID} (${CT_HOSTNAME}) …" CREATE_ARGS=( "$CTID" "$TMPL_REF" --hostname "$CT_HOSTNAME" --cores "$CORES" --memory "$RAM_MB" --swap "$SWAP_MB" --rootfs "${STORAGE}:${DISK_GB}" --net0 "$NETCFG" --features "nesting=1,keyctl=1" # Pflicht, damit Docker im unpriv. LXC läuft --unprivileged 1 --onboot 1 --ostype debian ) [[ -n "$PASSWORD" ]] && CREATE_ARGS+=(--password "$PASSWORD") pct create "${CREATE_ARGS[@]}" msg "Container #${CTID} erstellt." info "Starte Container …" pct start "$CTID" info "Warte auf Netzwerk …" for _ in $(seq 1 30); do pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1 && break sleep 2 done # ═══ 4 · Basis-Pakete + Docker + Node installieren ══════════════════════════ # Node wird für scripts/generate-keys.mjs gebraucht (ANON/SERVICE-JWT-Keys). info "Installiere Basis-Pakete + Docker im Container (kann ein paar Minuten dauern) …" pct exec "$CTID" -- bash -c ' set -euo pipefail export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq ca-certificates curl git openssl nodejs >/dev/null curl -fsSL https://get.docker.com | sh >/dev/null 2>&1 systemctl enable --now docker >/dev/null 2>&1 || true ' msg "Docker + Node installiert." # ═══ 5 · Stack ins Container holen (Local-Push ODER git clone) ══════════════ pct exec "$CTID" -- rm -rf /opt/rapport pct exec "$CTID" -- mkdir -p /opt/rapport if [[ -n "$LOCAL_STACK" ]]; then [[ -f "$LOCAL_STACK" ]] || die "LOCAL_STACK '$LOCAL_STACK' nicht gefunden (auf dem Proxmox-Host)." info "Schiebe lokalen Stack in den Container ($LOCAL_STACK) …" pct push "$CTID" "$LOCAL_STACK" /tmp/rapport-stack.tar.gz pct exec "$CTID" -- tar -xzf /tmp/rapport-stack.tar.gz -C /opt/rapport pct exec "$CTID" -- rm -f /tmp/rapport-stack.tar.gz msg "Lokaler Stack nach /opt/rapport entpackt (kein Gitea)." else info "Klone Rapport-Stack ($REPO_URL @ $REPO_REF) …" pct exec "$CTID" -- git clone --branch "$REPO_REF" --depth 1 "$REPO_URL" /opt/rapport msg "Stack nach /opt/rapport geklont." fi # ═══ 6 · Container-IP ermitteln ═════════════════════════════════════════════ info "Ermittle Container-IP …" CT_IP="" for _ in $(seq 1 15); do CT_IP="$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')" [[ -n "$CT_IP" ]] && break sleep 2 done [[ -n "$CT_IP" ]] || die "Konnte keine Container-IP ermitteln." msg "Container-IP: $CT_IP" # ═══ 7 · .env erzeugen: Secrets, JWT-Keys, LAN-URLs ═════════════════════════ # Reihenfolge laut SERVER-CONTAINER/README: # .env aus .env.example → POSTGRES_PASSWORD + JWT_SECRET → ANON/SERVICE-Keys # (müssen aus DEM JWT_SECRET abgeleitet sein!) → SITE_URL/API_EXTERNAL_URL info "Generiere Secrets + JWT-Keys, setze LAN-URLs …" pct exec "$CTID" -- env CT_IP="$CT_IP" bash -c ' set -euo pipefail cd /opt/rapport cp -n .env.example .env PW=$(openssl rand -hex 32) JWT=$(openssl rand -hex 32) sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${PW}|" .env sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${JWT}|" .env # ANON_KEY + SERVICE_ROLE_KEY aus dem JWT_SECRET ableiten KEYS=$(node scripts/generate-keys.mjs 2>/dev/null) ANON=$(echo "$KEYS" | sed -n "s|^ANON_KEY=||p") SVC=$(echo "$KEYS" | sed -n "s|^SERVICE_ROLE_KEY=||p") [ -n "$ANON" ] && [ -n "$SVC" ] || { echo "Key-Generierung fehlgeschlagen" >&2; exit 1; } grep -q "^ANON_KEY=" .env && sed -i "s|^ANON_KEY=.*|ANON_KEY=${ANON}|" .env || echo "ANON_KEY=${ANON}" >> .env grep -q "^SERVICE_ROLE_KEY=" .env && sed -i "s|^SERVICE_ROLE_KEY=.*|SERVICE_ROLE_KEY=${SVC}|" .env || echo "SERVICE_ROLE_KEY=${SVC}" >> .env # LAN-URLs auf die Container-IP zeigen lassen sed -i "s|^SITE_URL=.*|SITE_URL=http://${CT_IP}:8080|" .env sed -i "s|^API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://${CT_IP}:8000|" .env # Standard-Ports (LXC ist dediziert, keine Konflikte wie auf Karims Dev-Mac) sed -i "s|^APP_PORT=.*|APP_PORT=8080|" .env 2>/dev/null || true sed -i "s|^KONG_HTTP_PORT=.*|KONG_HTTP_PORT=8000|" .env 2>/dev/null || true sed -i "s|^DB_PORT=.*|DB_PORT=5432|" .env 2>/dev/null || true chmod 600 .env ' msg ".env erzeugt (Secrets zufällig, Keys passend zum JWT_SECRET)." # ═══ 8 · DB-Migrations bereitstellen (Local-Push ODER sync-migrations) ══════ if [[ -n "$LOCAL_MIGRATIONS" ]]; then [[ -f "$LOCAL_MIGRATIONS" ]] || die "LOCAL_MIGRATIONS '$LOCAL_MIGRATIONS' nicht gefunden (auf dem Proxmox-Host)." info "Schiebe lokale Migrations in den Container ($LOCAL_MIGRATIONS) …" pct exec "$CTID" -- rm -rf /opt/rapport/volumes/db/init/rapport-migrations pct exec "$CTID" -- mkdir -p /opt/rapport/volumes/db/init/rapport-migrations pct push "$CTID" "$LOCAL_MIGRATIONS" /tmp/rapport-migrations.tar.gz pct exec "$CTID" -- tar -xzf /tmp/rapport-migrations.tar.gz -C /opt/rapport/volumes/db/init/rapport-migrations pct exec "$CTID" -- rm -f /tmp/rapport-migrations.tar.gz msg "Lokale Migrations bereitgestellt (kein Gitea)." else info "Hole DB-Migrations aus dem App-Repo …" pct exec "$CTID" -- bash -c "cd /opt/rapport && bash scripts/sync-migrations.sh" msg "Migrations synchronisiert." fi # ═══ 9 · Stack hochfahren ═══════════════════════════════════════════════════ info "Starte Compose-Stack (Images pullen, Erststart ~1-2 Min) …" pct exec "$CTID" -- bash -c "cd /opt/rapport && docker compose up -d" # ═══ Fertig ═════════════════════════════════════════════════════════════════ echo msg "RAPPORT Server läuft in LXC #${CTID} (${CT_HOSTNAME})" echo -e " ${GN}Frontend:${CL} http://${CT_IP}:8080" echo -e " ${GN}API/Kong:${CL} http://${CT_IP}:8000" echo -e " ${GN}Postgres:${CL} ${CT_IP}:5432 (nur containerintern; PW in /opt/rapport/.env)" echo info "Status: pct exec ${CTID} -- bash -c 'cd /opt/rapport && docker compose ps'" info "Logs: pct exec ${CTID} -- bash -c 'cd /opt/rapport && docker compose logs -f'" info "Shell: pct enter ${CTID}"