diff --git a/CHANGELOG.md b/CHANGELOG.md index 50c3dcf..8a007c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.20.0] – 2026-10-03 + +### Hinzugefügt +- `deploy/update.sh` und `deploy/backup.sh` erkennen die Installationsart selbst und beherrschen jetzt auch die Installation als Dienst (Node und PostgreSQL direkt im Container) – nicht mehr nur Docker (#031) +- Eine fehlgeschlagene Sicherung bricht die Aktualisierung ab, bevor etwas verändert wird (#031) + +### Behoben +- Die Zustandsprüfung des Web-Containers konnte nie gelingen: Next bindet im Standalone-Modus an die Adresse aus `HOSTNAME`, und Docker setzt dort den Containernamen – der Server lauschte nur auf der Container-IP. `web` blieb dauerhaft auf „health: starting“ (#031) + ## [0.19.2] – 2026-10-03 ### Sicherheit diff --git a/DEVLOG.md b/DEVLOG.md index c9edaaf..a41409f 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 15 h 23 min +**Gesamt:** 16 h 08 min | Issue | Zeit | |---|---| @@ -36,6 +36,7 @@ | #028 Passkeys | 0 h 45 min | | #029 Einrichtungsdokumentation | 0 h 40 min | | #030 netbird Reverse Proxy | 0 h 20 min | +| #031 Update für beide Varianten | 0 h 45 min | | Meilenstein | Zeit | |---|---| @@ -58,7 +59,7 @@ ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.19.2 +**Version:** 0.20.0 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -169,6 +170,9 @@ - `npm audit` vor der Freigabe erneut laufen lassen: In den zwei Wochen seit dem letzten Stand kam eine kritische Meldung zu Next.js dazu (RCE in `next/og`). Wir nutzen `next/og` nicht, waren also nicht angreifbar – angehoben auf 16.3.8, Tests und Build grün - Lehre für den Betrieb: Vor jedem Produktivgang und danach regelmäßig `npm audit` prüfen, nicht nur bei Änderungen am Code +- Update- und Sicherungsskript für beide Installationsarten (#031). Beim Durchspielen zwei Fehler gefunden: Die Zustandsprüfung des Web-Containers konnte nie gelingen (Next bindet an `HOSTNAME`, Docker setzt dort den Containernamen), und eine fehlgeschlagene Sicherung hielt die Aktualisierung nicht auf +- Lehre: Ein Skript, das nie unter echten Bedingungen gelaufen ist, ist eine Behauptung. Erst der Durchlauf gegen einen laufenden Stapel hat beide Fehler gezeigt + ### Nächster Schritt - #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline - Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023) diff --git a/Dockerfile b/Dockerfile index 6fdbedd..13ebd44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,9 @@ RUN npx prisma generate \ FROM basis AS laufzeit ENV NODE_ENV=production ENV PORT=3000 +# Ohne diese Zeile nimmt Next den von Docker gesetzten HOSTNAME als Bindeadresse und lauscht +# nur auf der Container-IP. Die Zustandsprüfung über 127.0.0.1 schlägt dann immer fehl. +ENV HOSTNAME=0.0.0.0 RUN addgroup -g 1001 -S proxledger && adduser -S proxledger -u 1001 -G proxledger diff --git a/deploy/backup.sh b/deploy/backup.sh index c1a7972..a8a41af 100755 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Sichert Datenbank und hochgeladene PDFs von ProxLedger. +# Sichert Datenbank, hochgeladene PDFs und die Konfiguration von ProxLedger. +# Erkennt selbst, ob die Installation mit Docker oder als Dienst im Container läuft. # # ./backup.sh /pfad/zum/sicherungsordner # @@ -13,17 +14,50 @@ 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}" +VERZEICHNIS="${COMPOSE_VERZEICHNIS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" mkdir -p "$ZIEL" -cd "$COMPOSE_VERZEICHNIS" +cd "$VERZEICHNIS" -echo "Sichere Datenbank …" -docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-proxledger}" "${POSTGRES_DB:-proxledger}" \ - | gzip > "${ZIEL}/datenbank-${STAND}.sql.gz" +# Siehe deploy/update.sh – der Dienst ist das eindeutigere Merkmal. +if systemctl cat proxledger.service >/dev/null 2>&1; then + ART="dienst" +else + ART="docker" +fi -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 ProxLedger (${ART}) nach ${ZIEL} …" + +if [ "$ART" = "docker" ]; then + 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 +else + # DATABASE_URL steht in der .env; pg_dump versteht sie direkt. + # shellcheck disable=SC1091 + set -o allexport + . ./.env + set +o allexport + + [ -n "${DATABASE_URL:-}" ] || { + echo "DATABASE_URL fehlt in der .env – ohne sie keine Sicherung." >&2 + exit 1 + } + + echo "Sichere Datenbank …" + pg_dump "$DATABASE_URL" | gzip > "${ZIEL}/datenbank-${STAND}.sql.gz" + + echo "Sichere hochgeladene Dateien …" + UPLOADS="${UPLOAD_DIR:-${VERZEICHNIS}/data/uploads}" + if [ -d "$UPLOADS" ]; then + tar -czf "${ZIEL}/uploads-${STAND}.tar.gz" -C "$(dirname "$UPLOADS")" "$(basename "$UPLOADS")" + else + echo " (kein Upload-Verzeichnis unter ${UPLOADS})" + fi +fi echo "Sichere Konfiguration …" cp .env "${ZIEL}/env-${STAND}.txt" diff --git a/deploy/lxc-installer.sh b/deploy/lxc-installer.sh index bf99a82..8c18eb8 100755 --- a/deploy/lxc-installer.sh +++ b/deploy/lxc-installer.sh @@ -181,4 +181,5 @@ printf ' 3. Für den Betrieb über HTTPS einen Reverse Proxy davorsetzen (Beisp 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" +printf ' Aktualisieren: pct exec %s -- bash -c "bash /opt/proxledger/deploy/update.sh"\n' "$CTID" +printf ' Sichern: pct exec %s -- bash -c "bash /opt/proxledger/deploy/backup.sh /var/backups/proxledger"\n' "$CTID" diff --git a/deploy/update.sh b/deploy/update.sh index d94d775..5a43e84 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# ProxLedger aktualisieren (Docker-Installation). +# ProxLedger aktualisieren – erkennt selbst, ob die Installation mit Docker +# oder als Dienst im Container (Node + PostgreSQL direkt) läuft. # -# bash deploy/update.sh # Quelltext holen, neu bauen, migrieren, neu starten +# bash deploy/update.sh # sichern, holen, bauen, migrieren, neu starten # bash deploy/update.sh --ohne-sicherung # # Vor dem Aktualisieren wird gesichert – das dauert Sekunden und erspart schlechte Abende. @@ -23,38 +24,105 @@ fehler() { SICHERN="ja" [ "${1:-}" = "--ohne-sicherung" ] && SICHERN="nein" -command -v docker >/dev/null 2>&1 || fehler "Docker nicht gefunden." -[ -f .env ] || fehler "Keine .env gefunden – wird dieses Skript im richtigen Verzeichnis ausgeführt?" +# Wohin gesichert wird. Der Standardort gehört root; wer anders arbeitet, setzt SICHERUNG_ZIEL. +SICHERUNG_ZIEL="${SICHERUNG_ZIEL:-/var/backups/proxledger}" +# ---------------------------------------------------------------- Art der Installation +# +# Der systemd-Dienst ist das eindeutigere Merkmal: Die docker-compose.yml liegt auch im +# Quelltext der Dienst-Installation, ein Dienst namens proxledger aber nur dort. +installationsart() { + if systemctl cat proxledger.service >/dev/null 2>&1; then + printf 'dienst' + elif command -v docker >/dev/null 2>&1 && [ -f docker-compose.yml ]; then + printf 'docker' + fi +} + +ART=$(installationsart) +[ -n "$ART" ] || fehler "Keine Installation gefunden – weder der Dienst proxledger noch Docker mit docker-compose.yml." + +blau "Installationsart: ${ART}" + +# ---------------------------------------------------------------- Sicherung if [ "$SICHERN" = "ja" ]; then - blau "Sicherung anlegen …" - bash deploy/backup.sh + blau "Sicherung anlegen nach ${SICHERUNG_ZIEL} …" + if ! bash deploy/backup.sh "$SICHERUNG_ZIEL"; then + fehler "Die Sicherung ist fehlgeschlagen – deshalb wird nicht aktualisiert. + Häufigste Ursache: fehlende Rechte am Zielordner. Mögliche Wege: + sudo bash deploy/update.sh + SICHERUNG_ZIEL=/pfad/mit/rechten bash deploy/update.sh + bash deploy/update.sh --ohne-sicherung (nur wenn anderweitig gesichert ist)" + fi gruen "Sicherung abgeschlossen." +else + printf '\033[1;33m! Ohne Sicherung – auf eigenes Risiko.\033[0m\n' fi +# ---------------------------------------------------------------- Quelltext if [ -d .git ]; then blau "Neuen Stand holen …" git pull --ff-only +else + printf '\033[1;33m! Kein Git-Verzeichnis – der Quelltext bleibt wie er ist.\033[0m\n' fi -blau "Abbilder neu bauen …" -docker compose build --pull +# ---------------------------------------------------------------- Aktualisieren +if [ "$ART" = "docker" ]; then + [ -f .env ] || fehler "Keine .env gefunden – wird dieses Skript im richtigen Verzeichnis ausgeführt?" -blau "Datenbank migrieren …" -docker compose run --rm migrate + blau "Abbilder neu bauen …" + docker compose build --pull -blau "Dienste neu starten …" -docker compose up -d + blau "Datenbank migrieren …" + docker compose run --rm migrate -# Kurz warten und nachsehen, ob das Dashboard wieder antwortet. + blau "Dienste neu starten …" + docker compose up -d +else + command -v npm >/dev/null 2>&1 || fehler "npm nicht gefunden – gehört zur Installation als Dienst." + [ -f .env ] || fehler "Keine .env gefunden (erwartet in ${VERZEICHNIS})." + + # Die .env trägt APP_SECRET; ein git pull fasst sie nicht an, aber sicher ist sicher. + cp .env "/tmp/proxledger-env-$(date +%s).sicherung" + + blau "Abhängigkeiten installieren …" + npm ci + + blau "Datenbank migrieren …" + npx prisma generate + npx prisma migrate deploy + + blau "Anwendung bauen …" + npm run build + npm run build:worker + + blau "Dienste neu starten …" + systemctl restart proxledger proxledger-worker +fi + +# ---------------------------------------------------------------- Prüfen blau "Warte auf das Dashboard …" -for versuch in $(seq 1 30); do - if docker compose exec -T web wget -qO- http://localhost:3000/api/gesundheit >/dev/null 2>&1; then +PORT="${PORT:-3000}" + +for versuch in $(seq 1 45); do + if [ "$ART" = "docker" ]; then + erreichbar=$(docker compose exec -T web wget -qO- "http://127.0.0.1:3000/api/gesundheit" >/dev/null 2>&1 && echo ja || echo nein) + else + erreichbar=$(curl -fsS -o /dev/null --max-time 2 "http://localhost:${PORT}/api/gesundheit" && echo ja || echo nein) + fi + + if [ "$erreichbar" = "ja" ]; then gruen "ProxLedger läuft wieder (nach ${versuch} s)." - docker compose ps + [ "$ART" = "docker" ] && docker compose ps + [ "$ART" = "dienst" ] && systemctl --no-pager --lines=0 status proxledger proxledger-worker | grep -E "●|Active:" exit 0 fi sleep 1 done -fehler "Das Dashboard antwortet nicht. Protokoll ansehen: docker compose logs -n 50 web" +if [ "$ART" = "docker" ]; then + fehler "Das Dashboard antwortet nicht. Protokoll ansehen: docker compose logs -n 50 web" +else + fehler "Das Dashboard antwortet nicht. Protokoll ansehen: journalctl -u proxledger -n 50" +fi diff --git a/docker-compose.yml b/docker-compose.yml index 05bb636..0663e9d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,7 +60,7 @@ services: volumes: - uploads:/data healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/gesundheit"] + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/gesundheit"] interval: 30s timeout: 5s retries: 5 diff --git a/docs/anleitung/README.md b/docs/anleitung/README.md index c2ffab5..de21064 100644 --- a/docs/anleitung/README.md +++ b/docs/anleitung/README.md @@ -396,31 +396,46 @@ Rückweg immer nutzbar. **Sichern:** ```bash -./deploy/backup.sh /var/backups/proxledger +bash /opt/proxledger/deploy/backup.sh /var/backups/proxledger ``` -Das Skript sichert Datenbank, hochgeladene PDFs und die `.env`. Für regelmäßige Sicherungen in die -Crontab eintragen: +Das Skript sichert Datenbank, hochgeladene PDFs und die `.env` – und erkennt selbst, ob die +Installation mit Docker oder als Dienst im Container läuft. Für regelmäßige Sicherungen in die Crontab: ``` 30 2 * * * /opt/proxledger/deploy/backup.sh /var/backups/proxledger ``` -**Zurückspielen:** +**Zurückspielen** (Docker): ```bash gunzip -c datenbank-20260917-023000.sql.gz | docker compose exec -T postgres psql -U proxledger proxledger ``` +Bei der Installation als Dienst stattdessen: + +```bash +gunzip -c datenbank-20260917-023000.sql.gz | psql "$DATABASE_URL" +``` + **Aktualisieren:** ```bash -cd /opt/proxledger -git pull -docker compose up -d --build +bash /opt/proxledger/deploy/update.sh ``` -Migrationen laufen dabei automatisch. +Ein Befehl für beide Installationsarten. Der Reihe nach: sichern, neuen Stand holen, bauen, +migrieren, neu starten – und zum Schluss prüfen, ob das Dashboard wieder antwortet. Schlägt die +Sicherung fehl, bricht das Skript ab, **bevor** etwas verändert wird. + +| Aufruf | Wirkung | +|---|---| +| `bash deploy/update.sh` | der übliche Weg | +| `SICHERUNG_ZIEL=/pfad bash deploy/update.sh` | anderer Ort für die Sicherung | +| `bash deploy/update.sh --ohne-sicherung` | nur, wenn anderweitig gesichert ist | + +Beim LXC-Weg nach Art der Helper-Scripts geht es auch über das Skript auf dem Proxmox-Host – dort +erkennt das Gerüst eine vorhandene Installation und bietet die Aktualisierung an. > `APP_SECRET` unbedingt mitsichern: Ohne diesen Wert sind alle Sitzungen ungültig. Und beachte, dass eine > Sicherung die Zugangsdaten für SMTP und Webhook im Klartext enthält – sie gehört entsprechend geschützt. diff --git a/docs/betrieb/sicherung.md b/docs/betrieb/sicherung.md index 04a5e94..33daad5 100644 --- a/docs/betrieb/sicherung.md +++ b/docs/betrieb/sicherung.md @@ -14,10 +14,15 @@ Maschinenkennung, nicht an einem Token. ## Sichern ```bash -bash deploy/backup.sh # Datenbank und Uploads +bash deploy/backup.sh /var/backups/proxledger ``` -Beim LXC-Weg reicht zusätzlich die gewohnte Proxmox-Sicherung des Containers. +Das Skript erkennt selbst, ob die Installation mit Docker oder als Dienst läuft, und sichert in beiden +Fällen Datenbank, Uploads und `.env`. Beim LXC-Weg reicht zusätzlich die gewohnte Proxmox-Sicherung +des Containers. + +Vor jeder Aktualisierung läuft es ohnehin automatisch – [`deploy/update.sh`](../../deploy/update.sh) +ruft es als Erstes auf und bricht ab, wenn es fehlschlägt. ## Zurückspielen diff --git a/docs/issues/031-update-skript-beide-varianten.md b/docs/issues/031-update-skript-beide-varianten.md new file mode 100644 index 0000000..9271176 --- /dev/null +++ b/docs/issues/031-update-skript-beide-varianten.md @@ -0,0 +1,33 @@ +# #031 Update- und Sicherungsskript für beide Installationsarten +**Status:** erledigt +**Meilenstein:** v0.20.0 +**Labels:** deployment +**Geschätzt:** 2 h **Tatsächlich:** 0 h 45 min + +## Ziel +`deploy/update.sh` und `deploy/backup.sh` konnten nur Docker. Die Installation als Dienst (Node und +PostgreSQL direkt im LXC) ging leer aus – dort half nur der Weg über das Gerüst der community-scripts. + +## Aufgaben +- [x] Beide Skripte erkennen die Installationsart selbst (systemd-Dienst oder Docker) +- [x] Dienst-Weg: `npm ci`, `prisma migrate deploy`, bauen, `systemctl restart` +- [x] Sicherung für den Dienst-Weg: `pg_dump` über `DATABASE_URL`, Uploads aus `UPLOAD_DIR` +- [x] Fehlgeschlagene Sicherung bricht die Aktualisierung ab – **vor** jeder Änderung +- [x] Hinweis am Ende von `deploy/lxc-installer.sh` auf die Skripte umgestellt +- [x] Handbuch und Betriebsdoku angepasst +- [x] Docker-Weg vollständig durchgespielt + +## Zwei Fehler beim Durchspielen gefunden +1. **Die Zustandsprüfung des Web-Containers konnte nie gelingen.** Next bindet im Standalone-Modus an + die Adresse aus `HOSTNAME` – und Docker setzt darin den Containernamen. Der Server lauschte also + nur auf der Container-IP, nicht auf `127.0.0.1`. Der Dienst `web` blieb dauerhaft auf + „health: starting“; von außen war er trotzdem erreichbar, deshalb fiel es bisher nicht auf. + Behoben mit `ENV HOSTNAME=0.0.0.0` im Dockerfile; die Prüfungen verwenden jetzt `127.0.0.1` + statt `localhost` (das löst im Container auch auf `::1` auf). +2. **Die Sicherung scheiterte stumm an fehlenden Rechten** (`/var/backups` gehört root) und die + Aktualisierung lief trotzdem weiter. Jetzt bricht sie ab und nennt die drei Auswege. + +## Prüfung +Vollständiger Durchlauf gegen einen laufenden Stapel: Sicherung (Datenbank 6,3 KB mit 25 Tabellen, +Uploads, `.env`), Abbilder gebaut, migriert, neu gestartet, Dashboard nach zwei Sekunden wieder da. +Danach stand `web` zum ersten Mal auf **healthy**. diff --git a/docs/issues/README.md b/docs/issues/README.md index 558e3a9..1432b0a 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -34,4 +34,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#028](028-passkeys.md) | Anmeldung mit Passkeys | erledigt | v0.18.0 | 4 h | 0 h 45 min | | [#029](029-betriebsdokumentation.md) | Einrichtungsdokumentation | erledigt | v0.19.0 | 3 h | 0 h 40 min | | [#030](030-netbird-reverse-proxy.md) | netbird Reverse Proxy in die Anleitung | erledigt | v0.19.1 | 1 h | 0 h 20 min | -| | **Summe** | | | **147 h** | **15 h 23 min** | +| [#031](031-update-skript-beide-varianten.md) | Update- und Sicherungsskript für beide Varianten | erledigt | v0.20.0 | 2 h | 0 h 45 min | +| | **Summe** | | | **149 h** | **16 h 08 min** | diff --git a/package.json b/package.json index 3b91a42..5bdaa78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.19.2", + "version": "0.20.0", "private": true, "scripts": { "dev": "next dev",