#!/usr/bin/env bash # # OPENBUREAU — All-in-One LXC für Proxmox VE (Supabase-Kern + CMS). # # AUSFÜHREN AUF DEM PROXMOX-HOST (nicht im Container), als root: # bash create-openbureau-lxc.sh # # Legt einen unprivileged Debian-12-LXC an (Docker-fähig: nesting + keyctl), # installiert Docker, zieht das Repo, generiert alle Secrets (POSTGRES_PASSWORD, # JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY) und befüllt die .env. Optional baut # und startet es den Stack direkt. set -euo pipefail ############################ CONFIG ############################ # Alle Werte sind per Umgebungsvariable überschreibbar, z.B.: # ROOTFS_STORAGE=local-zfs HOSTNAME=openbureau-dev SITE_DOMAIN=dev.openbureau.ch \ # IP=192.168.1.134/24 GATEWAY=192.168.1.1 bash create-openbureau-lxc.sh CTID="${CTID:-$(pvesh get /cluster/nextid)}" HOSTNAME="${HOSTNAME:-openbureau}" # Storage TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" ROOTFS_STORAGE="${ROOTFS_STORAGE:-local-lvm}" DISK_GB="${DISK_GB:-20}" # Supabase + CMS # Ressourcen RAM_MB="${RAM_MB:-4096}" SWAP_MB="${SWAP_MB:-1024}" CORES="${CORES:-2}" # Netzwerk BRIDGE="${BRIDGE:-vmbr0}" IP="${IP:-dhcp}" # "dhcp" ODER statisch z.B. "192.168.1.50/24" GATEWAY="${GATEWAY:-}" # nur bei statischer IP # Öffentliche Domain hinter einem Reverse-Proxy (Caddy o.ä.) mit Pfad-Routing # (/auth/* + /rest/* → :8000, Rest → :8080). Leer = LAN-Direktzugriff per IP:Port. SITE_DOMAIN="${SITE_DOMAIN:-}" # Zugang SSH_PUBKEY_FILE="${SSH_PUBKEY_FILE:-$HOME/.ssh/id_ed25519.pub}" ROOT_PASSWORD="" # Repo ist öffentlich → Clone braucht KEIN Token. # GIT_TOKEN nur setzen, wenn das CMS später per GIT_PUBLISH nach Gitea # zurückschreiben soll (git push braucht Auth). Format: "tokenname:tokenwert". GIT_TOKEN="${GIT_TOKEN:-}" REPO_HOST="git.kgva.ch/karim/OPENBUREAU.git" APP_DIR="/opt/openbureau" # Admin (sieht/bearbeitet ALLE Beiträge). Wird auch als erster Login-User vorgeschlagen. ADMIN_EMAIL="${ADMIN_EMAIL:-karim@gabrielevarano.ch}" # Stack nach dem Setup direkt bauen + starten? COMPOSE_UP="true" ################################################################## say() { echo -e "\n\033[1;36m▸ $*\033[0m"; } # --- 0. Interaktiv nachfragen (Enter = Default) -------------------------- # Übersprungen, wenn kein Terminal (z.B. CI) — dann gelten die Defaults oben # bzw. vorab gesetzte Umgebungsvariablen. if [ -t 0 ]; then echo "OPENBUREAU LXC-Setup — Enter übernimmt den Default in [Klammern]." read -rp " Storage für die Disk [${ROOTFS_STORAGE}]: " _x; ROOTFS_STORAGE="${_x:-$ROOTFS_STORAGE}" read -rp " Netzwerk-Bridge [${BRIDGE}]: " _x; BRIDGE="${_x:-$BRIDGE}" read -rp " IP (dhcp | x.x.x.x/24) [${IP}]: " _x; IP="${_x:-$IP}" [ "$IP" != "dhcp" ] && { read -rp " Gateway: " GATEWAY; } read -rp " Admin-E-Mail [${ADMIN_EMAIL}]: " _x; ADMIN_EMAIL="${_x:-$ADMIN_EMAIL}" fi # --- 1. Template sicherstellen ------------------------------------------- say "Suche aktuelles Debian-12-Template…" pveam update >/dev/null || true TEMPLATE="$(pveam available --section system \ | awk '/debian-12-standard/{print $2}' | sort -V | tail -1)" [ -n "$TEMPLATE" ] || { echo "Kein debian-12-Template gefunden."; exit 1; } if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE"; then say "Lade Template $TEMPLATE…" pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" fi TEMPLATE_REF="${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" # --- 2. Netzwerk ---------------------------------------------------------- if [ "$IP" = "dhcp" ]; then NET="name=eth0,bridge=${BRIDGE},ip=dhcp" else [ -n "$GATEWAY" ] || { echo "Statische IP, aber GATEWAY leer."; exit 1; } NET="name=eth0,bridge=${BRIDGE},ip=${IP},gw=${GATEWAY}" fi # --- 3. Container erstellen ---------------------------------------------- say "Erstelle LXC $CTID ($HOSTNAME)…" CREATE_ARGS=( "$CTID" "$TEMPLATE_REF" --hostname "$HOSTNAME" --cores "$CORES" --memory "$RAM_MB" --swap "$SWAP_MB" --rootfs "${ROOTFS_STORAGE}:${DISK_GB}" --net0 "$NET" --unprivileged 1 --features "nesting=1,keyctl=1" --onboot 1 ) [ -n "$ROOT_PASSWORD" ] && CREATE_ARGS+=(--password "$ROOT_PASSWORD") [ -f "$SSH_PUBKEY_FILE" ] && CREATE_ARGS+=(--ssh-public-keys "$SSH_PUBKEY_FILE") pct create "${CREATE_ARGS[@]}" say "Starte Container…" pct start "$CTID" sleep 5 # Clone-URL (mit Token, falls gesetzt) if [ -n "$GIT_TOKEN" ]; then REPO_URL="https://${GIT_TOKEN}@${REPO_HOST}" else REPO_URL="https://${REPO_HOST}" fi # --- 4. Provisionierung im Container ------------------------------------- say "Installiere Docker + Git, ziehe Repo, generiere Secrets…" pct exec "$CTID" -- bash -euo pipefail -c " export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq ca-certificates curl git openssl >/dev/null curl -fsSL https://get.docker.com | sh >/dev/null systemctl enable --now docker if [ ! -d '${APP_DIR}/.git' ]; then git clone --quiet '${REPO_URL}' '${APP_DIR}' || { echo 'WARN: Clone fehlgeschlagen (Token nötig?). Setup hier gestoppt.'; exit 0; } fi cd '${APP_DIR}/cms' if [ ! -f .env ]; then cp .env.example .env # Secrets generieren 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 via Wegwerf-Node-Container ableiten KEYS=\$(docker run --rm -v \"\$PWD\":/w -w /w node:20-alpine \ node scripts/generate-keys.mjs \"\$JWT\" 2>/dev/null) ANON=\$(echo \"\$KEYS\" | sed -n 's/^ANON_KEY=//p') SVC=\$(echo \"\$KEYS\" | sed -n 's/^SERVICE_ROLE_KEY=//p') sed -i \"s|^ANON_KEY=.*|ANON_KEY=\${ANON}|\" .env sed -i \"s|^SERVICE_ROLE_KEY=.*|SERVICE_ROLE_KEY=\${SVC}|\" .env # URLs setzen — bei gesetzter SITE_DOMAIN auf die öffentliche HTTPS-Domain # (Browser ruft /auth/* + /rest/* same-origin auf, der Proxy routet sie an # :8000), sonst auf die Container-IP fürs LAN. HOSTIP=\$(hostname -I | awk '{print \$1}') SITE_DOMAIN='${SITE_DOMAIN}' if [ -n \"\$SITE_DOMAIN\" ]; then SITE_URL=\"https://\$SITE_DOMAIN\"; API_URL=\"https://\$SITE_DOMAIN\" else SITE_URL=\"http://\${HOSTIP}:8080\"; API_URL=\"http://\${HOSTIP}:8000\" fi sed -i \"s|^SITE_URL=.*|SITE_URL=\${SITE_URL}|\" .env sed -i \"s|^API_EXTERNAL_URL=.*|API_EXTERNAL_URL=\${API_URL}|\" .env sed -i \"s|^ADMIN_EMAILS=.*|ADMIN_EMAILS=${ADMIN_EMAIL}|\" .env # Auf allen Interfaces lauschen, damit Reverse-Proxy bzw. LAN drankommen. sed -i \"s|^BIND_ADDR=.*|BIND_ADDR=0.0.0.0|\" .env # CORS auf die Browser-Origin (= SITE_URL) festnageln statt „*\". sed -i \"s|__CORS_ORIGIN__|\${SITE_URL}|g\" kong.yml echo 'OK: .env generiert.' fi # Der CMS-Container läuft als non-root (uid 1000). Das gemountete Repo muss # ihm gehören, damit Hugo public/ bauen und content/ schreiben kann. chown -R 1000:1000 '${APP_DIR}' if [ '${COMPOSE_UP}' = 'true' ]; then echo '→ Baue + starte Stack (dauert beim ersten Mal ein paar Minuten)…' docker compose up -d --build fi # Tägliches DB-Backup (3:15 Uhr) — Dialog-Daten liegen NUR in Postgres. printf 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n15 3 * * * root cd ${APP_DIR}/cms && bash scripts/backup-db.sh >> /var/log/openbureau-backup.log 2>&1\n' > /etc/cron.d/openbureau-backup echo 'OK: tägliches DB-Backup eingerichtet (/etc/cron.d/openbureau-backup).' " # --- 5. Abschluss -------------------------------------------------------- IPADDR="$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')" say "Fertig. LXC $CTID läuft${IPADDR:+ unter $IPADDR}." if [ -n "$SITE_DOMAIN" ]; then cat <} zeigt) Caddy-Block: ${SITE_DOMAIN} { # Nur /auth/* muss public ans Gateway (Browser-Login). Daten # laufen über /api/* (Node spricht kong intern an). /rest, /storage, # /realtime NICHT exponieren — unnötige Angriffsfläche. @auth path /auth/* reverse_proxy @auth ${IPADDR:-}:8000 reverse_proxy ${IPADDR:-}:8080 } EOF fi cat <}:8080/admin/ Live: http://${IPADDR:-}:8080/ Supabase: http://${IPADDR:-}:8000 (nur API-Gateway, keine Web-UI — / gibt 404, ist normal) Login-User anlegen (im Container, nach dem Start): pct enter ${CTID} cd ${APP_DIR}/cms source .env curl -s -X POST "http://localhost:8000/auth/v1/admin/users" \\ -H "apikey: \$SERVICE_ROLE_KEY" \\ -H "Authorization: Bearer \$SERVICE_ROLE_KEY" \\ -H "Content-Type: application/json" \\ -d '{"email":"${ADMIN_EMAIL}","password":"DEIN-PASSWORT","email_confirm":true}' Hinweise: • :8000 ist das Supabase-API-Gateway (Kong), keine Web-Oberfläche. Das Admin-Login (:8080/admin/) spricht im Hintergrund damit. • Für Domain/HTTPS: SITE_URL + API_EXTERNAL_URL in .env auf die öffentliche Adresse setzen und 'docker compose up -d --build' neu. • Logs: pct enter ${CTID}; cd ${APP_DIR}/cms; docker compose logs -f EOF