#!/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 https://git.kgva.ch/karim/rapport-server-proxmox-lxc/raw/branch/main/rapport-lxc.sh)" # # …oder lokal: bash rapport-lxc.sh # # Alle Parameter sind per Env-Var überschreibbar, z.B.: # RAM_MB=8192 NET_IP=192.168.1.50/24 NET_GW=192.168.1.1 bash rapport-lxc.sh # ───────────────────────────────────────────────────────────────────────────── 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:-https://git.kgva.ch/karim/rapport-server.git}" REPO_REF="${REPO_REF:-main}" TEMPLATE="${TEMPLATE:-debian-12-standard}" # pveam-Template-Name (Präfix) # ═══ 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." # ═══ 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 · Repo klonen ════════════════════════════════════════════════════════ info "Klone Rapport-Stack ($REPO_URL @ $REPO_REF) …" pct exec "$CTID" -- bash -c " set -euo pipefail rm -rf /opt/rapport git clone --branch '$REPO_REF' --depth 1 '$REPO_URL' /opt/rapport " msg "Stack nach /opt/rapport geklont." # ═══ 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 holen ════════════════════════════════════════════════ info "Hole DB-Migrations aus dem App-Repo …" pct exec "$CTID" -- bash -c "cd /opt/rapport && bash scripts/sync-migrations.sh" msg "Migrations synchronisiert." # ═══ 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}"