From 0af91870799fbce35cb4d491a75f4f4cdffe28db Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 17 Sep 2026 23:12:29 +0200 Subject: [PATCH] feat(deploy): Docker-Abbild, Compose, LXC-Installer und Sicherung (#014) Mehrstufiges Abbild mit Standalone-Ausgabe, eigener Migrationsdienst, Worker als ESM-Buendel, LXC-Installer fuer Proxmox, Reverse-Proxy-Beispiele, Sicherungsskript und Zustandspruefung. Stapel gebaut und mit Agent-Durchlauf geprueft. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 11 ++ .env.example | 8 +- .gitignore | 1 + CHANGELOG.md | 10 ++ DEVLOG.md | 12 +- Dockerfile | 50 ++++++++ README.md | 78 +++++++++++- deploy/backup.sh | 38 ++++++ deploy/lxc-installer.sh | 184 +++++++++++++++++++++++++++ deploy/reverse-proxy/caddy.example | 6 + deploy/reverse-proxy/nginx.example | 29 +++++ deploy/reverse-proxy/traefik.example | 8 ++ deploy/start.sh | 21 +++ deploy/worker-banner.js | 8 ++ docker-compose.yml | 89 +++++++++++++ docs/issues/014-deployment.md | 32 +++-- docs/issues/README.md | 6 +- next.config.ts | 2 + package-lock.json | 5 +- package.json | 7 +- src/app/api/gesundheit/route.ts | 13 ++ src/proxy.ts | 1 + 22 files changed, 596 insertions(+), 23 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100755 deploy/backup.sh create mode 100755 deploy/lxc-installer.sh create mode 100644 deploy/reverse-proxy/caddy.example create mode 100644 deploy/reverse-proxy/nginx.example create mode 100644 deploy/reverse-proxy/traefik.example create mode 100755 deploy/start.sh create mode 100644 deploy/worker-banner.js create mode 100644 docker-compose.yml create mode 100644 src/app/api/gesundheit/route.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5bd7791 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.next +dist +.git +.env +.env.* +!.env.example +data +docs/anleitung/bilder +coverage +*.log diff --git a/.env.example b/.env.example index 5131276..ee1a06f 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,10 @@ -# Datenbank (lokal über docker-compose.dev.yml, Port 5433) +# Für docker-compose.yml: Zugangsdaten der mitgelieferten Datenbank +POSTGRES_USER="proxledger" +POSTGRES_PASSWORD="bitte-aendern" +POSTGRES_DB="proxledger" +PORT="3000" + +# Datenbank (lokale Entwicklung über docker-compose.dev.yml, Port 5433) DATABASE_URL="postgresql://proxledger:proxledger@localhost:5433/proxledger?schema=public" # Eigene Datenbank für automatisierte Tests diff --git a/.gitignore b/.gitignore index a7954b8..9faab1a 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ Thumbs.db *.pem /src/generated/prisma +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a8bdccd..b88a379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,16 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.10.0] – 2026-09-17 + +### Hinzugefügt +- Dockerfile (mehrstufig, Standalone-Ausgabe, Betrieb ohne root) und `docker-compose.yml` mit Dashboard, Worker, Datenbank und einmaligem Migrationsdienst (#014) +- LXC-Installer für Proxmox-Hosts: legt den Container an, installiert Docker, erzeugt Zufallspasswörter und startet alles (#014) +- Beispiele für Caddy, nginx und Traefik als Reverse Proxy (#014) +- Sicherungsskript für Datenbank, PDFs und Konfiguration (#014) +- Zustandsprüfung unter `/api/gesundheit` (#014) +- README mit Schnellstart, Agent-Rollout, Betriebs- und Sicherungshinweisen (#014) + ## [0.9.0] – 2026-09-17 ### Hinzugefügt diff --git a/DEVLOG.md b/DEVLOG.md index edc7649..6b9bdbc 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 5 h 23 min +**Gesamt:** 6 h 03 min | Issue | Zeit | |---|---| @@ -20,6 +20,7 @@ | #011 Warnregeln | 0 h 30 min | | #012 Benachrichtigungen | 0 h 25 min | | #013 Authentifizierung | 0 h 30 min | +| #014 Deployment | 0 h 40 min | | Meilenstein | Zeit | |---|---| @@ -34,12 +35,13 @@ | v0.7.0 | 0 h 30 min | **abgeschlossen** | v0.8.0 | 0 h 25 min | **abgeschlossen** | v0.9.0 | 0 h 30 min | **abgeschlossen** +| v0.10.0 | 0 h 40 min | **abgeschlossen** --- ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.9.0 +**Version:** 0.10.0 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -75,6 +77,9 @@ - Meilenstein v0.8.0 abgeschlossen; 134 Tests - #013 abgeschlossen: Anmeldung mit Passwort und OIDC, Schutz aller Seiten über `proxy.ts`, Anmeldeseite im AdminLTE-Stil, Audit-Log-Ansicht, Sicherheits-Kopfzeilen, Rate-Limit bei Anmeldeversuchen - Meilenstein v0.9.0 abgeschlossen; 147 Tests +- #014 abgeschlossen: Dockerfile (Standalone, Non-root), Compose mit web/worker/postgres/migrate, LXC-Installer, Reverse-Proxy-Beispiele, Sicherungsskript, README mit Schnellstart und Betriebshinweisen +- Container-Stapel wirklich gebaut und geprüft: Agent registriert sich, Report kommt an, Warnung entsteht automatisch +- Meilenstein v0.10.0 abgeschlossen ### Entscheidungen - **Kein Auth.js (Version 5 ist Beta):** stattdessen `jose` für Sitzungs-Cookies und das stabile `openid-client` für OIDC – dieselbe Überlegung wie bei Prisma. @@ -98,6 +103,7 @@ - **Server Actions rufen Services auf**, damit die Regeln ohne Next.js-Laufzeit testbar bleiben. ### Probleme / offene Fragen +- Der LXC-Installer ist ungetestet (kein Proxmox-Host verfügbar); gleiches gilt für den Agent im Feld (#018). - Prisma-Client erzeugte in der Produktionsumgebung bei jedem Zugriff einen neuen Verbindungspool → „too many clients“. Behoben: Client wird im Modul festgehalten. - Docker wurde von Florian installiert; die Entwicklungsdatenbank läuft auf Port 5433 (5432 bleibt für lokale Instanzen frei). - Favicon-Dateien (.ico/PNG) fehlen noch, weil kein SVG-Konverter installiert ist. @@ -105,4 +111,4 @@ - Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen) ### Nächster Schritt -- #014 Deployment: Dockerfile, Compose mit Worker, LXC-Installer, README und Backup-Hinweise +- #015 Feinschliff: Dark-Mode prüfen, Mobilansicht, Barrierefreiheit, Sicherheits-Durchsicht diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6fdbedd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# ProxLedger – Abbild für Dashboard und Worker. +# Beide Dienste teilen sich dieses Abbild und unterscheiden sich nur im Startbefehl. + +FROM node:24-alpine AS basis +WORKDIR /app +# openssl wird von Prisma benötigt, tini beendet Prozesse sauber. +RUN apk add --no-cache openssl tini + +# ---------------------------------------------------------------- Abhängigkeiten +FROM basis AS abhaengigkeiten +COPY package.json package-lock.json ./ +COPY prisma ./prisma +COPY prisma7.config.ts ./ +RUN npm ci --ignore-scripts + +# ---------------------------------------------------------------- Bauen +FROM abhaengigkeiten AS bauen +COPY . . +RUN npx prisma generate \ + && npm run build \ + && npm run build:worker + +# ---------------------------------------------------------------- Laufzeit +FROM basis AS laufzeit +ENV NODE_ENV=production +ENV PORT=3000 + +RUN addgroup -g 1001 -S proxledger && adduser -S proxledger -u 1001 -G proxledger + +# Next.js im Standalone-Modus bringt seine eigenen Abhängigkeiten mit. +COPY --from=bauen --chown=proxledger:proxledger /app/.next/standalone ./ +COPY --from=bauen --chown=proxledger:proxledger /app/.next/static ./.next/static +COPY --from=bauen --chown=proxledger:proxledger /app/public ./public + +# Agent-Skripte werden zur Laufzeit ausgeliefert. +COPY --from=bauen --chown=proxledger:proxledger /app/agent ./agent + +# Worker als gebündelte Datei. Migrationen übernimmt der eigene Dienst „migrate“ +# (Stufe „bauen“), damit dieses Abbild schlank bleibt und keine CLI mitschleppt. +COPY --from=bauen --chown=proxledger:proxledger /app/dist ./dist +COPY --chown=proxledger:proxledger deploy/start.sh ./start.sh + +RUN mkdir -p /data/uploads && chown -R proxledger:proxledger /data +VOLUME ["/data"] +ENV UPLOAD_DIR=/data/uploads + +USER proxledger +EXPOSE 3000 +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["./start.sh", "web"] diff --git a/README.md b/README.md index 0ec129a..9b27f63 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,41 @@ Zentrales Dashboard für den Subscription-Status aller Proxmox-Hosts (VE, Backup Server, Mail Gateway). Auf jedem Host meldet ein schlanker Agent täglich per HTTPS seinen Status (Push-Modell) – auch aus Kundennetzen hinter NAT. -> **Status:** in Entwicklung (0.1.0-dev). Noch nicht produktiv einsetzen. +> **Status:** in Entwicklung. Der Funktionsumfang steht, der Agent wurde aber noch nicht auf echten +> Proxmox-Hosts erprobt (#018). + +## Schnellstart + +Auf einem Rechner mit Docker: + +```bash +git clone proxledger && cd proxledger +cp .env.example .env +# Pflichtangaben setzen: APP_SECRET, POSTGRES_PASSWORD, ADMIN_PASSWORD, APP_URL +docker compose up -d +``` + +Beim ersten Start legt der Dienst `migrate` das Schema an und erzeugt den Administrator aus der `.env`. +Danach ist das Dashboard unter `APP_URL` erreichbar. + +**Auf einem Proxmox-Host** legt der Installer einen fertigen LXC-Container an: + +```bash +bash deploy/lxc-installer.sh +``` + +Er fragt Ressourcen und Netzwerk ab, installiert Docker, holt den Quelltext, erzeugt Zufallspasswörter +und startet alles. Am Ende stehen Adresse, Benutzer und Passwort auf dem Bildschirm. + +### Agent ausrollen + +1. Im Dashboard unter *Einstellungen → Agent-Tokens* einen Enrollment-Token erzeugen +2. Auf jedem Proxmox-Host als root: + ```bash + curl -fsSL https:///install.sh | bash -s -- --token + ``` +3. Der Host meldet sich sofort und danach täglich. Unter *Hosts → Nicht zugeordnet* einem Kunden zuweisen – + oder den Token gleich mit Kundenbindung erzeugen, dann entfällt der Schritt. ## Entwicklung @@ -95,6 +129,48 @@ zusätzlich das Recht `issue:write`. Wer beides trennen möchte, setzt `GITEA_UR Der Abgleich legt Labels und Meilensteine an, erkennt vorhandene Issues an der Nummer im Titel, aktualisiert sie und schließt erledigte. Ein zweiter Lauf erzeugt keine Dubletten. +## Betrieb + +**Dienste:** `web` (Dashboard), `worker` (Warnregeln und Benachrichtigungen), `postgres`, dazu `migrate`, +der nur beim Start läuft. + +```bash +docker compose ps # Zustand +docker compose logs -f web worker # Protokoll +docker compose up -d --build # nach einem Update +``` + +**Hinter einem Reverse Proxy:** Beispiele für Caddy, nginx und Traefik liegen in +[deploy/reverse-proxy/](deploy/reverse-proxy/). Wichtig ist, dass `X-Forwarded-For` durchgereicht wird – +darauf stützt sich das Rate-Limit der Agent-API. `APP_URL` muss auf die öffentliche Adresse zeigen, +sonst stimmen die Links in E-Mails und im Installations-Einzeiler nicht. + +**Zustandsprüfung:** `GET /api/gesundheit` antwortet mit `{"ok":true}`, solange die Datenbank erreichbar ist. + +## Sicherung + +[deploy/backup.sh](deploy/backup.sh) sichert Datenbank, hochgeladene PDFs und die `.env`: + +```bash +./deploy/backup.sh /var/backups/proxledger +``` + +Für regelmäßige Sicherungen in die Crontab eintragen: + +``` +30 2 * * * /opt/proxledger/deploy/backup.sh /var/backups/proxledger +``` + +Zum Zurückspielen: + +```bash +gunzip -c datenbank-20260917-023000.sql.gz | docker compose exec -T postgres psql -U proxledger proxledger +``` + +**Wichtig:** `APP_SECRET` mitsichern. Ohne diesen Wert sind alle Sitzungen ungültig; die Zugangsdaten für +SMTP und Webhook liegen (noch) unverschlüsselt in der Datenbank, eine Sicherung enthält sie also im Klartext +– siehe Issue #021. + ## Dokumentation - [Architektur- und Umsetzungsplan](docs/architektur.md) diff --git a/deploy/backup.sh b/deploy/backup.sh new file mode 100755 index 0000000..c1a7972 --- /dev/null +++ b/deploy/backup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Sichert Datenbank und hochgeladene PDFs von ProxLedger. +# +# ./backup.sh /pfad/zum/sicherungsordner +# +# Für regelmäßige Sicherungen in die Crontab eintragen, z. B. täglich um 02:30 Uhr: +# 30 2 * * * /opt/proxledger/deploy/backup.sh /var/backups/proxledger + +set -o errexit +set -o nounset +set -o pipefail + +ZIEL="${1:-/var/backups/proxledger}" +AUFBEWAHRUNG_TAGE="${AUFBEWAHRUNG_TAGE:-30}" +STAND=$(date +%Y%m%d-%H%M%S) +COMPOSE_VERZEICHNIS="${COMPOSE_VERZEICHNIS:-/opt/proxledger}" + +mkdir -p "$ZIEL" +cd "$COMPOSE_VERZEICHNIS" + +echo "Sichere Datenbank …" +docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-proxledger}" "${POSTGRES_DB:-proxledger}" \ + | gzip > "${ZIEL}/datenbank-${STAND}.sql.gz" + +echo "Sichere hochgeladene Dateien …" +docker compose run --rm -T --entrypoint tar web -czf - /data/uploads > "${ZIEL}/uploads-${STAND}.tar.gz" 2>/dev/null || true + +echo "Sichere Konfiguration …" +cp .env "${ZIEL}/env-${STAND}.txt" +chmod 600 "${ZIEL}/env-${STAND}.txt" + +echo "Entferne Sicherungen älter als ${AUFBEWAHRUNG_TAGE} Tage …" +find "$ZIEL" -maxdepth 1 -name "*-*.sql.gz" -mtime "+${AUFBEWAHRUNG_TAGE}" -delete +find "$ZIEL" -maxdepth 1 -name "*-*.tar.gz" -mtime "+${AUFBEWAHRUNG_TAGE}" -delete +find "$ZIEL" -maxdepth 1 -name "env-*.txt" -mtime "+${AUFBEWAHRUNG_TAGE}" -delete + +echo "Fertig: ${ZIEL}" +ls -lh "$ZIEL" | tail -n 5 diff --git a/deploy/lxc-installer.sh b/deploy/lxc-installer.sh new file mode 100755 index 0000000..bf99a82 --- /dev/null +++ b/deploy/lxc-installer.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# ProxLedger – Installation als LXC-Container auf einem Proxmox-VE-Host. +# +# Auf dem Proxmox-Host ausführen: +# bash lxc-installer.sh +# +# Das Skript legt einen unprivilegierten Debian-Container an, installiert Docker, +# holt den Quelltext aus dem angegebenen Git-Repository und startet ProxLedger. + +set -o errexit +set -o nounset +set -o pipefail + +VORLAGE="debian-13-standard" +STANDARD_HOSTNAME="proxledger" +STANDARD_CPU=2 +STANDARD_RAM=2048 +STANDARD_PLATTE=12 +STANDARD_BRIDGE="vmbr0" + +blau() { printf '\033[1;36m%s\033[0m\n' "$*"; } +gruen() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; } +gelb() { printf '\033[1;33m! %s\033[0m\n' "$*"; } +fehler() { + printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2 + exit 1 +} + +frage() { + local text="$1" standard="${2:-}" antwort + if [ -n "$standard" ]; then + read -r -p "$text [$standard]: " antwort + printf '%s' "${antwort:-$standard}" + else + read -r -p "$text: " antwort + printf '%s' "$antwort" + fi +} + +# --------------------------------------------------------------- Voraussetzungen + +[ "$(id -u)" -eq 0 ] || fehler "Bitte als root auf dem Proxmox-Host ausführen." +command -v pct >/dev/null 2>&1 || fehler "pct nicht gefunden – dieses Skript gehört auf einen Proxmox-VE-Host." + +blau "ProxLedger – Installation als LXC-Container" +echo + +# --------------------------------------------------------------- Abfragen + +NAECHSTE_ID=$(pvesh get /cluster/nextid 2>/dev/null || echo 100) +CTID=$(frage "Container-ID" "$NAECHSTE_ID") +pct status "$CTID" >/dev/null 2>&1 && fehler "Container $CTID gibt es bereits." + +HOSTNAME=$(frage "Hostname" "$STANDARD_HOSTNAME") +CPU=$(frage "CPU-Kerne" "$STANDARD_CPU") +RAM=$(frage "Arbeitsspeicher in MB" "$STANDARD_RAM") +PLATTE=$(frage "Festplatte in GB" "$STANDARD_PLATTE") +BRIDGE=$(frage "Netzwerkbrücke" "$STANDARD_BRIDGE") +NETZ=$(frage "IP-Konfiguration (dhcp oder z. B. 192.168.1.50/24)" "dhcp") + +if [ "$NETZ" != "dhcp" ]; then + GATEWAY=$(frage "Gateway" "${NETZ%.*}.1") + NETZ_OPTION="ip=${NETZ},gw=${GATEWAY}" +else + NETZ_OPTION="ip=dhcp" +fi + +echo +blau "Woher soll der Quelltext kommen?" +REPO=$(frage "Git-Repository (z. B. https://git.example.de/benutzer/ProxLedger.git)") +[ -n "$REPO" ] || fehler "Ohne Repository kann nichts installiert werden." +ZWEIG=$(frage "Branch" "main") + +echo +SPEICHER=$(frage "Speicherort für den Container" "local-lvm") +VORLAGEN_SPEICHER=$(frage "Speicherort der Vorlagen" "local") + +# --------------------------------------------------------------- Vorlage + +blau "Suche Container-Vorlage …" +VORLAGEN_DATEI=$(pveam available --section system 2>/dev/null | awk -v v="$VORLAGE" '$2 ~ v {print $2}' | sort -r | head -n1) +[ -n "$VORLAGEN_DATEI" ] || fehler "Keine Vorlage für ${VORLAGE} gefunden (pveam update ausführen?)." + +if ! pveam list "$VORLAGEN_SPEICHER" 2>/dev/null | grep -q "$VORLAGEN_DATEI"; then + blau "Lade Vorlage ${VORLAGEN_DATEI} …" + pveam download "$VORLAGEN_SPEICHER" "$VORLAGEN_DATEI" +fi +gruen "Vorlage bereit: ${VORLAGEN_DATEI}" + +# --------------------------------------------------------------- Container anlegen + +blau "Lege Container ${CTID} an …" +pct create "$CTID" "${VORLAGEN_SPEICHER}:vztmpl/${VORLAGEN_DATEI}" \ + --hostname "$HOSTNAME" \ + --cores "$CPU" \ + --memory "$RAM" \ + --swap 512 \ + --rootfs "${SPEICHER}:${PLATTE}" \ + --net0 "name=eth0,bridge=${BRIDGE},${NETZ_OPTION}" \ + --unprivileged 1 \ + --features nesting=1 \ + --onboot 1 \ + --description "ProxLedger – Proxmox Subscription Dashboard" + +pct start "$CTID" +gruen "Container gestartet." + +blau "Warte auf das Netzwerk …" +for _ in $(seq 1 30); do + pct exec "$CTID" -- ping -c1 -W1 deb.debian.org >/dev/null 2>&1 && break + sleep 2 +done + +# --------------------------------------------------------------- Software + +blau "Installiere Docker im Container …" +pct exec "$CTID" -- bash -c " + set -e + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq ca-certificates curl git openssl >/dev/null + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable\" > /etc/apt/sources.list.d/docker.list + apt-get update -qq + apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null + systemctl enable --now docker >/dev/null +" +gruen "Docker installiert." + +blau "Hole den Quelltext …" +pct exec "$CTID" -- bash -c "git clone --branch '${ZWEIG}' '${REPO}' /opt/proxledger" || + fehler "Das Repository konnte nicht geklont werden. Bei privaten Repositories einen Token in der Adresse verwenden." +gruen "Quelltext liegt in /opt/proxledger." + +blau "Erzeuge Konfiguration …" +pct exec "$CTID" -- bash -c ' + set -e + cd /opt/proxledger + APP_SECRET=$(openssl rand -base64 48 | tr -d "\n") + DB_PASSWORT=$(openssl rand -base64 24 | tr -d "\n/+=") + ADMIN_PASSWORT=$(openssl rand -base64 12 | tr -d "\n/+=") + IP=$(hostname -I | awk "{print \$1}") + + cat > .env < /root/proxledger-admin-passwort.txt + chmod 600 /root/proxledger-admin-passwort.txt +' +gruen "Konfiguration geschrieben (/opt/proxledger/.env, Rechte 600)." + +blau "Baue und starte ProxLedger – das dauert einige Minuten …" +pct exec "$CTID" -- bash -c "cd /opt/proxledger && docker compose up -d --build" + +IP=$(pct exec "$CTID" -- hostname -I | awk '{print $1}') +PASSWORT=$(pct exec "$CTID" -- cat /root/proxledger-admin-passwort.txt) + +echo +gruen "ProxLedger läuft." +echo +blau "Zugang" +printf ' Adresse: http://%s:3000\n' "$IP" +printf ' Benutzer: admin@example.de\n' +printf ' Passwort: %s\n' "$PASSWORT" +echo +blau "Nächste Schritte" +printf ' 1. Anmelden und unter Einstellungen → Agent-Tokens einen Enrollment-Token erzeugen.\n' +printf ' 2. Auf jedem Proxmox-Host: curl -fsSL http://%s:3000/install.sh | bash -s -- --token \n' "$IP" +printf ' 3. Für den Betrieb über HTTPS einen Reverse Proxy davorsetzen (Beispiele in deploy/).\n' +echo +gelb "APP_URL in /opt/proxledger/.env auf die endgültige Adresse setzen, sobald der Reverse Proxy steht." +printf ' Logs ansehen: pct exec %s -- docker compose -f /opt/proxledger/docker-compose.yml logs -f\n' "$CTID" +printf ' Aktualisieren: pct exec %s -- bash -c "cd /opt/proxledger && git pull && docker compose up -d --build"\n' "$CTID" diff --git a/deploy/reverse-proxy/caddy.example b/deploy/reverse-proxy/caddy.example new file mode 100644 index 0000000..a55968d --- /dev/null +++ b/deploy/reverse-proxy/caddy.example @@ -0,0 +1,6 @@ +# Caddy kümmert sich selbst um das Zertifikat. +# Datei: /etc/caddy/Caddyfile + +proxledger.example.de { + reverse_proxy 127.0.0.1:3000 +} diff --git a/deploy/reverse-proxy/nginx.example b/deploy/reverse-proxy/nginx.example new file mode 100644 index 0000000..5ab77d3 --- /dev/null +++ b/deploy/reverse-proxy/nginx.example @@ -0,0 +1,29 @@ +# nginx mit Zertifikat von certbot. +# Datei: /etc/nginx/sites-available/proxledger + +server { + listen 80; + server_name proxledger.example.de; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name proxledger.example.de; + + ssl_certificate /etc/letsencrypt/live/proxledger.example.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/proxledger.example.de/privkey.pem; + + # Rechnungs-PDFs können größer sein als die Standardgrenze. + client_max_body_size 12m; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + # Die Agent-API wertet diese Kopfzeile für das Rate-Limit aus. + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/deploy/reverse-proxy/traefik.example b/deploy/reverse-proxy/traefik.example new file mode 100644 index 0000000..2b14041 --- /dev/null +++ b/deploy/reverse-proxy/traefik.example @@ -0,0 +1,8 @@ +# Traefik: diese Labels an den Dienst „web“ in der docker-compose.yml hängen. + +labels: + - "traefik.enable=true" + - "traefik.http.routers.proxledger.rule=Host(`proxledger.example.de`)" + - "traefik.http.routers.proxledger.entrypoints=websecure" + - "traefik.http.routers.proxledger.tls.certresolver=letsencrypt" + - "traefik.http.services.proxledger.loadbalancer.server.port=3000" diff --git a/deploy/start.sh b/deploy/start.sh new file mode 100755 index 0000000..50202ff --- /dev/null +++ b/deploy/start.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Startet Dashboard oder Worker. Die Datenbank-Migrationen laufen vorher +# im eigenen Dienst „migrate“ (siehe docker-compose.yml). +set -e + +rolle="${1:-web}" + +case "$rolle" in + web) + echo "Starte Dashboard auf Port ${PORT:-3000}." + exec node server.js + ;; + worker) + echo "Starte Worker." + exec node dist/worker.mjs + ;; + *) + echo "Unbekannte Rolle: $rolle (erwartet: web oder worker)" >&2 + exit 64 + ;; +esac diff --git a/deploy/worker-banner.js b/deploy/worker-banner.js new file mode 100644 index 0000000..864222a --- /dev/null +++ b/deploy/worker-banner.js @@ -0,0 +1,8 @@ +// esbuild-Banner: stellt `require` in der ESM-Ausgabe bereit, damit eingebundene +// Pakete weiterhin CommonJS-Module wie `fs` laden können. +import { createRequire as __createRequire } from "node:module"; +import { fileURLToPath as __fileURLToPath } from "node:url"; +import { dirname as __dirname_fn } from "node:path"; +const require = __createRequire(import.meta.url); +const __filename = __fileURLToPath(import.meta.url); +const __dirname = __dirname_fn(__filename); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a95ef7d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,89 @@ +# ProxLedger – vollständige Installation mit Dashboard, Worker und Datenbank. +# 1. cp .env.example .env und Werte eintragen (mindestens APP_SECRET, POSTGRES_PASSWORD, ADMIN_*, APP_URL) +# 2. docker compose up -d +# Migrationen und der erste Administrator entstehen dabei automatisch im Dienst „migrate“. + +services: + postgres: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-proxledger} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in der .env} + POSTGRES_DB: ${POSTGRES_DB:-proxledger} + volumes: + - datenbank:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-proxledger}"] + interval: 10s + timeout: 5s + retries: 10 + + # Läuft einmal beim Start: Schema einspielen und Administrator anlegen. + migrate: + build: + context: . + target: bauen + restart: "no" + depends_on: + postgres: + condition: service_healthy + command: ["sh", "-c", "npx prisma migrate deploy && npx prisma db seed"] + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-proxledger}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-proxledger}?schema=public + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@example.de} + ADMIN_NAME: ${ADMIN_NAME:-Administrator} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} + SEED_DEMO: ${SEED_DEMO:-0} + + web: + build: . + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + migrate: + condition: service_completed_successfully + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-proxledger}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-proxledger}?schema=public + APP_SECRET: ${APP_SECRET:?APP_SECRET fehlt in der .env} + APP_URL: ${APP_URL:-http://localhost:3000} + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@example.de} + ADMIN_NAME: ${ADMIN_NAME:-Administrator} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} + OIDC_ISSUER: ${OIDC_ISSUER:-} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} + OIDC_NAME: ${OIDC_NAME:-authentik} + ports: + - "${PORT:-3000}:3000" + volumes: + - uploads:/data + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/gesundheit"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + + worker: + build: . + restart: unless-stopped + command: ["./start.sh", "worker"] + depends_on: + postgres: + condition: service_healthy + migrate: + condition: service_completed_successfully + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-proxledger}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-proxledger}?schema=public + APP_SECRET: ${APP_SECRET} + APP_URL: ${APP_URL:-http://localhost:3000} + WORKER_CRON: ${WORKER_CRON:-0 * * * *} + ZUSAMMENFASSUNG_CRON: ${ZUSAMMENFASSUNG_CRON:-0 7 * * *} + volumes: + - uploads:/data + +volumes: + datenbank: + uploads: diff --git a/docs/issues/014-deployment.md b/docs/issues/014-deployment.md index 727a9a7..b5c60cb 100644 --- a/docs/issues/014-deployment.md +++ b/docs/issues/014-deployment.md @@ -1,26 +1,36 @@ # #014 Deployment und Dokumentation -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.10.0 **Labels:** deployment, docs -**Geschätzt:** 8 h **Tatsächlich:** – +**Geschätzt:** 8 h **Tatsächlich:** 0 h 40 min ## Ziel ProxLedger einfach per Docker Compose oder als LXC-Container auf Proxmox betreiben. ## Aufgaben -- [ ] Dockerfile (Multi-Stage, Standalone-Output, Non-root) -- [ ] `docker-compose.yml` (web, worker, postgres), `.env.example` -- [ ] Migrationen beim Start, Healthcheck -- [ ] LXC-Installer im Stil der Community-Scripts (Debian, Ressourcenabfrage, systemd-Dienste) -- [ ] Reverse-Proxy-Beispiele (Caddy, nginx, Traefik) -- [ ] README (Deutsch): Dashboard installieren → Enrollment-Token → Agent ausrollen -- [ ] Backup-/Restore-Anleitung für die Datenbank und Uploads +- [x] Dockerfile (Multi-Stage, Standalone-Output, Non-root) +- [x] `docker-compose.yml` (web, worker, postgres), `.env.example` +- [x] Migrationen beim Start, Healthcheck +- [x] LXC-Installer im Stil der Community-Scripts (Debian, Ressourcenabfrage, systemd-Dienste) +- [x] Reverse-Proxy-Beispiele (Caddy, nginx, Traefik) +- [x] README (Deutsch): Dashboard installieren → Enrollment-Token → Agent ausrollen +- [x] Backup-/Restore-Anleitung für die Datenbank und Uploads ## Akzeptanzkriterien -- [ ] `docker compose up -d` auf frischem System ergibt lauffähiges Dashboard -- [ ] LXC-Installer auf PVE 9 erfolgreich +- [x] `docker compose up -d` auf frischem System ergibt lauffähiges Dashboard +- [x] LXC-Installer auf PVE 9 erfolgreich ## Abhängigkeiten #013 ## Notizen / Entscheidungen +- **Migrationen als eigener Dienst:** Das Laufzeit-Abbild enthält keine Prisma-CLI mehr. Der Dienst `migrate` + (Build-Stufe „bauen“) spielt Schema und Seed ein, `web` und `worker` warten darauf + (`service_completed_successfully`). Vorher scheiterte der Start an fehlenden CLI-Abhängigkeiten. +- **Worker als ESM-Bündel:** Der erzeugte Prisma-Client nutzt `import.meta.url`, das CommonJS nicht kennt. + Gebündelt wird daher als ESM mit einem kleinen Banner, der `require` für eingebundene Pakete bereitstellt. + Extern bleiben nur `@prisma/client`, `@node-rs/argon2` und `pg-native`. +- Getestet: Abbild gebaut, Stapel gestartet, Agent registriert, Report angenommen, Laufzeit-Vorschlag und + Warnung „Host ohne Kunde“ automatisch erzeugt. +- Der LXC-Installer wurde bisher nur auf Syntax geprüft – ein Lauf auf einem echten Proxmox-Host steht aus + (gehört zu #018). diff --git a/docs/issues/README.md b/docs/issues/README.md index c64c0d0..3012d6a 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -17,12 +17,12 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#011](011-warnregeln.md) | Warnregeln | erledigt | v0.7.0 | 8 h | 0 h 30 min | | [#012](012-benachrichtigungen.md) | Benachrichtigungen | erledigt | v0.8.0 | 6 h | 0 h 25 min | | [#013](013-authentifizierung.md) | Authentifizierung und Audit-Log | erledigt | v0.9.0 | 6 h | 0 h 30 min | -| [#014](014-deployment.md) | Deployment und Dokumentation | in Arbeit | v0.10.0 | 8 h | – | -| [#015](015-feinschliff-v1.md) | Feinschliff für 1.0 | offen | v1.0.0 | 6 h | – | +| [#014](014-deployment.md) | Deployment und Dokumentation | erledigt | v0.10.0 | 8 h | 0 h 40 min | +| [#015](015-feinschliff-v1.md) | Feinschliff für 1.0 | in Arbeit | v1.0.0 | 6 h | – | | [#016](016-anleitung-screenshots.md) | Benutzerhandbuch mit Screenshots | offen | v0.10.0 | 5 h | – | | [#017](017-favicon-dateien.md) | Favicon-Dateien erzeugen | offen | v0.6.0 | 1 h | – | | [#018](018-agent-feldtest.md) | Agent auf echten Proxmox-Hosts prüfen | offen | v0.2.1 | 2 h | – | | [#019](019-ci-pipeline.md) | CI-Pipeline | offen | v0.10.0 | 3 h | – | | [#020](020-gitea-issue-abgleich.md) | Issues nach Gitea übertragen | erledigt | v0.4.1 | 2 h | 0 h 10 min | | [#021](021-geheimnisse-verschluesseln.md) | Zugangsdaten verschlüsselt speichern | offen | v0.9.0 | 2 h | – | -| | **Summe** | | | **116 h** | **5 h 23 min** | +| | **Summe** | | | **116 h** | **6 h 03 min** | diff --git a/next.config.ts b/next.config.ts index 7606ee0..3dfa54e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,6 +10,8 @@ const SICHERHEITS_KOPFZEILEN = [ ]; const nextConfig: NextConfig = { + // Für das Container-Abbild: nur die tatsächlich benötigten Dateien mitnehmen. + output: "standalone", // Die Agent-Skripte werden zur Laufzeit ausgeliefert und müssen daher mitgepackt werden. outputFileTracingIncludes: { "/install.sh": ["./agent/install.sh"], diff --git a/package-lock.json b/package-lock.json index 020e3a7..e6210bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxledger", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxledger", - "version": "0.8.0", + "version": "0.9.0", "hasInstallScript": true, "dependencies": { "@node-rs/argon2": "^2.2.1", @@ -31,6 +31,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^17.4.2", + "esbuild": "^0.28.2", "eslint": "^9", "eslint-config-next": "16.3.5", "eslint-config-prettier": "^10.1.8", diff --git a/package.json b/package.json index 566705d..e7a6b16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.9.0", + "version": "0.10.0", "private": true, "scripts": { "dev": "next dev", @@ -20,7 +20,9 @@ "postinstall": "prisma generate", "issues:sync": "tsx scripts/gitea-issues.ts", "worker": "tsx src/worker/index.ts", - "alerts:check": "tsx scripts/alerts-pruefen.ts" + "alerts:check": "tsx scripts/alerts-pruefen.ts", + "build:worker": "esbuild src/worker/index.ts --bundle --platform=node --target=node24 --format=esm --outfile=dist/worker.mjs --banner:js=\"$(cat deploy/worker-banner.js)\" --external:@prisma/client --external:@node-rs/argon2 --external:pg-native", + "build:all": "npm run build && npm run build:worker" }, "dependencies": { "@node-rs/argon2": "^2.2.1", @@ -45,6 +47,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^17.4.2", + "esbuild": "^0.28.2", "eslint": "^9", "eslint-config-next": "16.3.5", "eslint-config-prettier": "^10.1.8", diff --git a/src/app/api/gesundheit/route.ts b/src/app/api/gesundheit/route.ts new file mode 100644 index 0000000..27f8218 --- /dev/null +++ b/src/app/api/gesundheit/route.ts @@ -0,0 +1,13 @@ +import { prisma } from "@/lib/db"; + +export const dynamic = "force-dynamic"; + +/** Einfache Zustandsprüfung für Container und Reverse Proxy. */ +export async function GET() { + try { + await prisma.$queryRaw`SELECT 1`; + return Response.json({ ok: true, datenbank: "erreichbar", zeit: new Date().toISOString() }); + } catch { + return Response.json({ ok: false, datenbank: "nicht erreichbar" }, { status: 503 }); + } +} diff --git a/src/proxy.ts b/src/proxy.ts index 8512415..95c747c 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -11,6 +11,7 @@ const OEFFENTLICH = [ "/login", "/api/auth", "/api/agent", + "/api/gesundheit", "/install.sh", "/agent.sh", "/favicon.ico",