Files
RAPPORT-SERVER-PROXMOX-LXC/rapport-lxc.sh
T
karim 929c5ab600 app-Image: Registry-Pull mit Build-Fallback statt blindem Build
Das Frontend-Image liegt in der Gitea-Registry (karim/rapport-app:main),
also pullen wir es bevorzugt (schnell, kein npm-Build im Container) und
bauen nur aus Dockerfile.app, wenn die Registry nicht erreichbar ist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:38:03 +02:00

281 lines
15 KiB
Bash
Executable File

#!/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 · Frontend-Image bereitstellen (Registry-Pull, Fallback: Build) ══════
# Das `app`-Image liegt in der Gitea-Registry (git.kgva.ch/karim/rapport-app:main).
# Bevorzugt pullen — schnell, kein npm-Build im Container. Klappt der Pull nicht
# (Registry offline / nicht erreichbar / Auth), baut Dockerfile.app das Frontend
# aus RAPPORT.git als Fallback.
info "Hole Frontend-Image aus der Registry (Fallback: lokaler Build) …"
if pct exec "$CTID" -- bash -c "cd /opt/rapport && docker compose pull app" 2>/dev/null; then
msg "Frontend-Image aus Registry gezogen."
else
info "Registry-Pull fehlgeschlagen — baue aus Dockerfile.app (npm build, dauert) …"
pct exec "$CTID" -- bash -c "cd /opt/rapport && docker compose build app"
msg "Frontend-Image lokal gebaut."
fi
# ═══ 10 · Stack hochfahren ══════════════════════════════════════════════════
info "Starte Compose-Stack (Supabase-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}"