Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.
Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.
Schwer:
- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
(/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.
Mittel:
- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
Graph-Leser sich im Kommentar darauf berief.
Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.
Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.
1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
407 lines
18 KiB
Markdown
407 lines
18 KiB
Markdown
# Sichern und Zurückspielen
|
||
|
||
Laufzettel ist ein Gedächtnis: was in einem archivierten Lauf steht, soll in zehn Jahren noch sagen
|
||
können, wer wann was getan hat. Dazu kommt ein handfester Grund, der auf Papier klebt — **jeder
|
||
gedruckte Aufkleber zeigt auf einen Tag-Code, der nur in der Datenbank existiert.** Ein Code wird nie
|
||
geändert und nie neu vergeben; ist die Datenbank weg, sind alle Aufkleber im Haus wertlos, und es
|
||
gibt keinen Weg, sie zurückzurechnen.
|
||
|
||
Die Sicherung ist auf beiden Betriebswegen eine Sache von zwei Befehlen. Das Zurückspielen ist
|
||
ebenfalls einfach, aber nur, wenn man es einmal geübt hat.
|
||
|
||
## Was gesichert werden muss
|
||
|
||
Drei Dinge, und nur diese drei:
|
||
|
||
| | Was | Wo |
|
||
|---|---|---|
|
||
| **1** | **Die Datenbank.** Läufe, Vorlagen, Schrittergebnisse, Kommentare, Geräte, Personen, Tag-Codes, Protokoll. | `pg_dump`, siehe unten |
|
||
| **2** | **Die Geheimnisse.** `APP_SECRET` und das Datenbank-Passwort. | Compose: die `.env` · Nativ: `/etc/laufzettel/laufzettel.env` |
|
||
| **3** | **Die Anhänge.** Die Dateien und Fotos an den Schritten. | Volume `laufzettel_anhaenge` · Nativ: `ANHAENGE_PFAD`, von `install.sh` auf `/var/lib/laufzettel/anhaenge` gesetzt |
|
||
|
||
> **Die Anhänge sind seit #032 dabei, und sie sind die Falle.** In der Datenbank stehen nur ihre
|
||
> **Metadaten** — Name, Typ, Größe, Prüfsumme. Die Dateien selbst liegen im Dateisystem. Eine
|
||
> Sicherung, die nur `pg_dump` macht, sieht vollständig aus und ist es nicht: nach dem Zurückspielen
|
||
> stehen die Anhänge in der Oberfläche, und jeder Klick darauf meldet, dass die Datei fehlt.
|
||
> Datenbank und Verzeichnis gehören **zum selben Zeitpunkt** gesichert.
|
||
|
||
**Warum die Geheimnisse dazugehören:** `APP_SECRET` verschlüsselt nicht nur die Sitzungs-Cookies,
|
||
sondern auch die gespeicherten LDAP-Bind-Passwörter. Spielst du eine Datenbank in eine Installation
|
||
mit neuem `APP_SECRET` zurück, sind alle Sitzungen ungültig — das ist harmlos, man meldet sich neu an
|
||
— **und jedes Bind-Passwort der Verzeichnisquellen ist unlesbar**. Der Verzeichnis-Abgleich schlägt
|
||
dann fehl, und das Passwort muss von Hand neu eingetragen werden. Merkt man erst, wenn der nächste
|
||
Abgleich fällig ist.
|
||
|
||
Sicher die beiden Dinge getrennt und leg das Geheimnis dahin, wo auch die restlichen Zugangsdaten des
|
||
Hauses liegen — in den Passwortmanager. Eine Datei mit `APP_SECRET` neben dem Datenbank-Abzug auf
|
||
derselben Netzwerkfreigabe hebt den Zweck der Verschlüsselung auf.
|
||
|
||
**Was nicht gesichert werden muss:** Der Quellcode kommt aus Git, `node_modules` und `.next`
|
||
entstehen beim Bauen neu, das PDF der Anleitung liegt am Release.
|
||
|
||
**Was der Abzug bewusst nicht enthält:** ein `{{generiertes_passwort}}`. Es wird bei jeder Anzeige neu
|
||
erzeugt und nie gespeichert — nicht im Schrittergebnis, nicht im Kommentar, nicht im Protokoll. Wer
|
||
ein so gesetztes Passwort braucht, findet es in der Sicherung nicht, und das ist Absicht.
|
||
|
||
> **Der Abzug enthält personenbezogene Daten.** Namen, E-Mail-Adressen und Abteilungen der
|
||
> Mitarbeiter, dazu argon2-Hashes der lokalen Passwörter. Er gehört verschlüsselt aufbewahrt und
|
||
> nicht auf eine offene Freigabe.
|
||
|
||
## Docker Compose
|
||
|
||
### Sichern
|
||
|
||
Der Datenbankport ist von außen geschlossen — das ändert nichts, die Sicherung läuft über den
|
||
Dienstnamen:
|
||
|
||
```bash
|
||
cd /opt/laufzettel
|
||
docker compose exec -T postgres pg_dump -U laufzettel laufzettel | gzip > sicherung-$(date +%F).sql.gz
|
||
```
|
||
|
||
`-T` ist wichtig: ohne den Schalter hängt Docker ein Terminal dazwischen, und das verdirbt die
|
||
Ausgabe. Im Ergebnis liegt eine Datei von einigen hundert Kilobyte bis wenigen Megabyte.
|
||
|
||
Wer lieber das Eigenformat von PostgreSQL will — es lässt sich selektiv und parallel zurückspielen:
|
||
|
||
```bash
|
||
docker compose exec -T postgres pg_dump -U laufzettel -Fc laufzettel > sicherung-$(date +%F).dump
|
||
```
|
||
|
||
Kurz prüfen, dass die Datei nicht leer ist:
|
||
|
||
```bash
|
||
gunzip -c sicherung-$(date +%F).sql.gz | head -5
|
||
gunzip -c sicherung-$(date +%F).sql.gz | grep -c 'COPY public'
|
||
```
|
||
|
||
Das Volume heißt `laufzettel_datenbank`. Es mit `docker run --volumes-from` wegzukopieren ist
|
||
**keine** gleichwertige Sicherung: ein Dateiabzug eines laufenden PostgreSQL ist im besten Fall
|
||
brauchbar und im schlechteren still beschädigt. Nimm `pg_dump`.
|
||
|
||
#### Und die Anhänge
|
||
|
||
Hier gilt das Umgekehrte: ein Dateiabzug ist genau richtig, denn es sind gewöhnliche Dateien.
|
||
|
||
```bash
|
||
docker run --rm \
|
||
-v laufzettel_anhaenge:/daten:ro \
|
||
-v "$PWD":/ziel \
|
||
debian:13 tar czf /ziel/anhaenge-$(date +%F).tar.gz -C /daten .
|
||
```
|
||
|
||
Kurz nachsehen, dass etwas drin ist:
|
||
|
||
```bash
|
||
tar tzf anhaenge-$(date +%F).tar.gz | head -5
|
||
tar tzf anhaenge-$(date +%F).tar.gz | wc -l
|
||
```
|
||
|
||
Ist die Datei leer und es gibt Anhänge in der Oberfläche, stimmt der Volume-Name nicht — `docker
|
||
volume ls | grep anhaenge` nennt den richtigen. Er trägt den Projektnamen als Präfix, und der kommt
|
||
aus dem Verzeichnisnamen.
|
||
|
||
### Zurückspielen
|
||
|
||
Die Datenbank muss dafür leer sein — ein Abzug in eine gefüllte Datenbank gespielt bringt lauter
|
||
Fehler über schon vorhandene Tabellen und hinterlässt einen Mischzustand.
|
||
|
||
```bash
|
||
cd /opt/laufzettel
|
||
|
||
# 1. Die Anwendung anhalten, damit niemand dazwischenschreibt
|
||
docker compose stop web
|
||
|
||
# 2. Datenbank leeren und neu anlegen
|
||
docker compose exec -T postgres dropdb -U laufzettel laufzettel
|
||
docker compose exec -T postgres createdb -U laufzettel -O laufzettel laufzettel
|
||
|
||
# 3. Abzug einspielen
|
||
gunzip -c sicherung-2026-09-24.sql.gz | docker compose exec -T postgres psql -U laufzettel laufzettel
|
||
|
||
# 4. Die Anhänge zurück ins Volume – **vorher leeren**, sonst bleiben Dateien
|
||
# liegen, auf die keine Zeile mehr zeigt
|
||
docker run --rm -v laufzettel_anhaenge:/daten debian:13 sh -c 'rm -rf /daten/* /daten/.[!.]*' || true
|
||
docker run --rm \
|
||
-v laufzettel_anhaenge:/daten \
|
||
-v "$PWD":/quelle:ro \
|
||
debian:13 tar xzf /quelle/anhaenge-2026-09-24.tar.gz -C /daten
|
||
|
||
# 5. Migrationen nachziehen – nötig, wenn der Abzug von einer älteren Fassung stammt
|
||
docker compose run --rm migrate pnpm exec prisma migrate deploy
|
||
|
||
# 6. Wieder anfahren
|
||
docker compose start web
|
||
curl -s http://127.0.0.1:3000/api/gesundheit
|
||
```
|
||
|
||
Bei einem `-Fc`-Abzug tritt an die Stelle von Schritt 3:
|
||
|
||
```bash
|
||
docker compose exec -T postgres pg_restore -U laufzettel -d laufzettel --no-owner < sicherung-2026-09-24.dump
|
||
```
|
||
|
||
**Den Seed brauchst du danach nicht.** Er würde nichts kaputt machen — er richtet ein und räumt nicht
|
||
auf —, aber alles, was er anlegen würde, steht schon im Abzug.
|
||
|
||
## Nativ (LXC, VM, Blech)
|
||
|
||
### Sichern
|
||
|
||
Über den Unix-Socket, als Systemnutzer `postgres`. Einen TCP-Port gibt es nicht.
|
||
|
||
```bash
|
||
sudo -u postgres pg_dump -Fc laufzettel > /var/backups/laufzettel-$(date +%F).dump
|
||
```
|
||
|
||
Und die Anhänge dazu — gewöhnliche Dateien, also `tar`:
|
||
|
||
```bash
|
||
# Der Pfad steht in ANHAENGE_PFAD; `deploy/install.sh` setzt ihn auf
|
||
# /var/lib/laufzettel/anhaenge. **Kein Rückfallwert** – siehe den Kasten darunter.
|
||
ANHAENGE=$(sudo sh -c '. /etc/laufzettel/laufzettel.env; echo "${ANHAENGE_PFAD:?ANHAENGE_PFAD fehlt in /etc/laufzettel/laufzettel.env}"')
|
||
sudo tar czf /var/backups/laufzettel-anhaenge-$(date +%F).tar.gz -C "$ANHAENGE" .
|
||
```
|
||
|
||
> **Warum hier kein `${ANHAENGE_PFAD:-…}` steht.** Es stand einmal eines, und es war falsch:
|
||
> `/opt/laufzettel/daten/anhaenge` passt zu keiner Installation. `deploy/install.sh` setzt
|
||
> `/var/lib/laufzettel/anhaenge`, und ohne die Variable nimmt die Anwendung `./daten/anhaenge`
|
||
> **relativ zu ihrem Arbeitsverzeichnis** `/opt/laufzettel/.next/standalone` — also noch etwas
|
||
> anderes.
|
||
>
|
||
> Ein geratener Pfad ist beim Sichern eine leere Datei und beim Zurückspielen ein Schaden: dort
|
||
> steht ein `rm -rf` davor. Fehlt die Variable, soll der Befehl **abbrechen** und nicht raten.
|
||
> Deshalb `:?` und nicht `:-`. Gefunden bei der Durchsicht von Phase 2, nachdem derselbe Fehler in
|
||
> dieser Datei schon einmal Daten gekostet hätte (#032).
|
||
|
||
Und das Geheimnis, einmalig und in den Passwortmanager:
|
||
|
||
```bash
|
||
sudo cat /etc/laufzettel/laufzettel.env
|
||
```
|
||
|
||
`sudo -u postgres` und nicht `psql -h localhost`: die Rolle `laufzettel` hat ein Passwort, aber
|
||
`postgres` kommt über die Peer-Authentifizierung am Socket ohne eines hinein — für ein Skript, das
|
||
ohne Eingabe laufen soll, ist das der einfachere Weg.
|
||
|
||
### Zurückspielen
|
||
|
||
```bash
|
||
# 1. Dienst anhalten
|
||
sudo systemctl stop laufzettel
|
||
|
||
# 2. Datenbank leeren und neu anlegen, Eigentümer bleibt die Rolle laufzettel
|
||
sudo -u postgres dropdb laufzettel
|
||
sudo -u postgres createdb -O laufzettel laufzettel
|
||
|
||
# 3. Abzug einspielen
|
||
sudo -u postgres pg_restore -d laufzettel --no-owner /var/backups/laufzettel-2026-09-24.dump
|
||
|
||
# 4. Die Anhänge zurück – **vorher leeren**, sonst bleiben Dateien liegen, auf
|
||
# die keine Zeile mehr zeigt
|
||
# Ohne ANHAENGE_PFAD bricht das hier ab – und zwar **vor** dem rm -rf. Ein
|
||
# geratener Pfad hiesse: das echte Verzeichnis bleibt leer, die Anwendung meldet
|
||
# „Die Datei zu … fehlt im Dateisystem“, und niemand weiss, warum.
|
||
ANHAENGE=$(sudo sh -c '. /etc/laufzettel/laufzettel.env; echo "${ANHAENGE_PFAD:?ANHAENGE_PFAD fehlt in /etc/laufzettel/laufzettel.env}"')
|
||
sudo rm -rf "$ANHAENGE"
|
||
sudo install -d -o laufzettel -g laufzettel -m 0750 "$ANHAENGE"
|
||
sudo tar xzf /var/backups/laufzettel-anhaenge-2026-09-24.tar.gz -C "$ANHAENGE"
|
||
sudo chown -R laufzettel:laufzettel "$ANHAENGE"
|
||
|
||
# 5. Migrationen nachziehen, wenn der Abzug älter ist als der Code
|
||
sudo bash /opt/laufzettel/deploy/install.sh
|
||
|
||
# 6. Wieder anfahren und nachsehen
|
||
sudo systemctl start laufzettel
|
||
curl -s http://127.0.0.1:3000/api/gesundheit
|
||
```
|
||
|
||
Schritt 5 ist der Installationsaufruf, weil er die Migrationen mitbringt und den Dienst gleich
|
||
neu startet — Schritt 6 ist dann nur noch die Gegenprobe.
|
||
|
||
**Der `chown` in Schritt 4 ist nicht Beiwerk.** `tar` legt die Dateien mit den Besitzern aus dem
|
||
Archiv an; läuft die Rücksicherung als `root`, gehören sie danach `root`, und der Dienstnutzer
|
||
`laufzettel` kommt nicht mehr an sie heran. Jeder Anhang meldet dann „Datei fehlt“, obwohl sie
|
||
daliegt. Willst du nur die Migrationen:
|
||
|
||
```bash
|
||
cd /opt/laufzettel
|
||
sudo -u laufzettel env $(sudo grep -v '^#' /etc/laufzettel/laufzettel.env | xargs) pnpm exec prisma migrate deploy
|
||
```
|
||
|
||
## Automatisch sichern
|
||
|
||
### Nativ, als systemd-Timer
|
||
|
||
`/etc/systemd/system/laufzettel-sicherung.service`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Laufzettel sichern
|
||
After=postgresql.service
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
# `root`, nicht `postgres`: der Timer sichert jetzt auch das Anhang-Verzeichnis,
|
||
# und das gehoert dem Dienstnutzer `laufzettel`. Der Datenbankteil laeuft
|
||
# weiterhin als `postgres`, ueber `runuser`.
|
||
User=root
|
||
ExecStart=/usr/local/sbin/laufzettel-sichern
|
||
ExecStartPost=/usr/bin/find /var/backups -name 'laufzettel-*' -mtime +30 -delete
|
||
```
|
||
|
||
`/usr/local/sbin/laufzettel-sichern`, Modus 0750:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# Datenbank und Anhaenge zum **selben** Zeitpunkt. Getrennt gesichert ergeben
|
||
# beide Haelften kein vollstaendiges Bild (#032).
|
||
set -Eeuo pipefail
|
||
tag=$(date +%F)
|
||
runuser -u postgres -- pg_dump -Fc laufzettel > "/var/backups/laufzettel-$tag.dump"
|
||
|
||
# shellcheck source=/dev/null
|
||
. /etc/laufzettel/laufzettel.env
|
||
# Kein Rueckfallwert: ein geratener Pfad ergibt eine leere Sicherung, und die
|
||
# faellt erst beim Zuruckspielen auf.
|
||
anhaenge="${ANHAENGE_PFAD:?ANHAENGE_PFAD fehlt in /etc/laufzettel/laufzettel.env}"
|
||
if [ -d "$anhaenge" ]; then
|
||
tar czf "/var/backups/laufzettel-anhaenge-$tag.tar.gz" -C "$anhaenge" .
|
||
fi
|
||
```
|
||
|
||
`/etc/systemd/system/laufzettel-sicherung.timer`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Laufzettel taeglich sichern
|
||
|
||
[Timer]
|
||
OnCalendar=daily
|
||
# Nach einem Ausfall die verpasste Sicherung nachholen, statt sie zu ueberspringen.
|
||
Persistent=true
|
||
RandomizedDelaySec=15m
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
```
|
||
|
||
Einrichten, einschalten, und **einmal von Hand auslösen** — eine Sicherung, die
|
||
nur im Timer steht, hat noch nie eine Datei erzeugt:
|
||
|
||
```bash
|
||
sudo install -m 0750 laufzettel-sichern /usr/local/sbin/laufzettel-sichern
|
||
sudo install -d -o postgres -g postgres -m 0700 /var/backups
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now laufzettel-sicherung.timer
|
||
sudo systemctl start laufzettel-sicherung.service
|
||
ls -l /var/backups/laufzettel-*
|
||
sudo systemctl list-timers laufzettel-sicherung.timer
|
||
```
|
||
|
||
Nach dem Lauf müssen **zwei** Dateien daliegen: `laufzettel-<Datum>.dump` und
|
||
`laufzettel-anhaenge-<Datum>.tar.gz`. Fehlt die zweite, gibt es entweder keine Anhänge, oder
|
||
`ANHAENGE_PFAD` zeigt woandershin als gedacht.
|
||
|
||
### Compose, als Cron-Eintrag
|
||
|
||
In der Crontab von root (`sudo crontab -e`):
|
||
|
||
```cron
|
||
15 3 * * * cd /opt/laufzettel && docker compose exec -T postgres pg_dump -U laufzettel laufzettel | gzip > /var/backups/laufzettel-$(date +\%F).sql.gz && find /var/backups -name 'laufzettel-*.sql.gz' -mtime +30 -delete
|
||
```
|
||
|
||
Das `%` muss in einer Crontab als `\%` geschrieben werden, sonst schneidet cron die Zeile ab — der
|
||
häufigste Fehler an dieser Stelle, und er fällt erst auf, wenn die Datei fehlt.
|
||
|
||
Und die Anhänge direkt danach, damit beide Hälften vom selben Zeitpunkt stammen:
|
||
|
||
```cron
|
||
20 3 * * * docker run --rm -v laufzettel_anhaenge:/daten:ro -v /var/backups:/ziel debian:13 tar czf /ziel/laufzettel-anhaenge-$(date +\%F).tar.gz -C /daten . && find /var/backups -name 'laufzettel-anhaenge-*' -mtime +30 -delete
|
||
```
|
||
|
||
### Und dann weg vom Rechner
|
||
|
||
Eine Sicherung, die auf derselben Platte liegt wie die Datenbank, hilft gegen einen Tippfehler und
|
||
gegen nichts sonst. Zieh die Dateien nachts auf ein anderes System — Bordmittel genügen:
|
||
|
||
```bash
|
||
rsync -a --delete /var/backups/ sicherung@nas:/laufzettel/
|
||
```
|
||
|
||
Verschlüsselt, wegen der personenbezogenen Daten. Und behalte mehr als eine: ein Schaden, der drei
|
||
Tage unbemerkt bleibt, überschreibt sonst die letzte brauchbare Fassung.
|
||
|
||
## Einmal üben, sonst gilt es nicht
|
||
|
||
Eine Sicherung, die nie zurückgespielt wurde, ist keine Sicherung, sondern eine Datei. Der Seed
|
||
liefert eigens eine Vorlage **Restore-Test** mit — nimm sie und mach den Test einmal im Quartal zu
|
||
einem Lauf mit Datum und Ergebnis. Dann steht er im Archiv, mit Namen und Zeitpunkt, und niemand muss
|
||
sich erinnern, wann er zuletzt geprüft hat.
|
||
|
||
Der Test selbst, ohne den Betrieb anzufassen — in eine zweite, leere Datenbank:
|
||
|
||
```bash
|
||
# Compose
|
||
docker compose exec -T postgres createdb -U laufzettel probe
|
||
gunzip -c sicherung-2026-09-24.sql.gz | docker compose exec -T postgres psql -U laufzettel probe
|
||
docker compose exec -T postgres psql -U laufzettel probe -c 'SELECT count(*) FROM "Lauf"'
|
||
docker compose exec -T postgres dropdb -U laufzettel probe
|
||
```
|
||
|
||
```bash
|
||
# Nativ
|
||
sudo -u postgres createdb probe
|
||
sudo -u postgres pg_restore -d probe --no-owner /var/backups/laufzettel-2026-09-24.dump
|
||
sudo -u postgres psql -d probe -c 'SELECT count(*) FROM "Lauf"'
|
||
sudo -u postgres dropdb probe
|
||
```
|
||
|
||
Kommt bei `count(*)` eine plausible Zahl heraus, ist die Sicherung brauchbar. Kommt eine Fehlermeldung
|
||
oder eine Null, hast du es jetzt erfahren und nicht im Ernstfall.
|
||
|
||
### Und die Anhänge mitprüfen
|
||
|
||
Die Zahl in der Datenbank gegen die Zahl der Dateien halten — passt sie nicht, fehlt eine der beiden
|
||
Hälften:
|
||
|
||
```bash
|
||
# Compose: Zeilen in der Datenbank …
|
||
docker compose exec -T postgres psql -U laufzettel laufzettel -tAc 'SELECT count(*) FROM "Anhang"'
|
||
# … gegen Dateien im Archiv
|
||
tar tzf anhaenge-2026-09-24.tar.gz | grep -c '\.' || true
|
||
```
|
||
|
||
Stimmen die Zahlen nicht überein, ist entweder der Abzug älter als das Archiv oder umgekehrt. Beides
|
||
zum **selben Zeitpunkt** sichern — am einfachsten im selben Skript, direkt hintereinander.
|
||
|
||
## Umzug auf einen neuen Server
|
||
|
||
Derselbe Weg, nur mit einer Zwischenstation:
|
||
|
||
1. Auf dem alten System `pg_dump` ziehen und `APP_SECRET` sowie die restliche `.env` beziehungsweise
|
||
`laufzettel.env` mitnehmen
|
||
2. Auf dem neuen System frisch einrichten — [Compose](inbetriebnahme.md) oder
|
||
[nativ](proxmox.md)
|
||
3. **Vor** dem Zurückspielen `APP_SECRET` in der neuen Umgebungsdatei auf den alten Wert setzen, sonst
|
||
sind die LDAP-Bind-Passwörter unlesbar
|
||
4. Abzug zurückspielen wie oben, Migrationen nachziehen
|
||
5. `APP_URL` prüfen: bleibt der DNS-Name gleich, bleiben die gedruckten Aufkleber gültig. Ändert er
|
||
sich, lies den Anfang von [`reverse-proxy.md`](reverse-proxy.md), bevor du umstellst
|
||
|
||
Die PostgreSQL-Hauptfassung sollte auf dem Zielsystem dieselbe oder eine neuere sein. Ein Abzug aus
|
||
17 lässt sich in 18 einspielen, umgekehrt nicht.
|
||
|
||
## Wenn es klemmt
|
||
|
||
| Symptom | Ursache und was zu tun ist |
|
||
|---|---|
|
||
| `the input device is not a TTY` | Beim `docker compose exec` fehlt `-T`. |
|
||
| Der Abzug ist wenige Bytes groß | `pg_dump` hat nicht die Datenbank erwischt, sondern eine Fehlermeldung geschrieben. Ohne Umleitung wiederholen und lesen, was es sagt. |
|
||
| `ERROR: relation "Benutzer" already exists` | Du spielst in eine gefüllte Datenbank. Erst `dropdb`/`createdb`, siehe oben. |
|
||
| `role "laufzettel" does not exist` beim Zurückspielen auf einem anderen System | Der Abzug nennt Eigentümer, die dort fehlen. `pg_restore --no-owner` beziehungsweise `pg_dump --no-owner` beim Erzeugen. |
|
||
| Nach dem Zurückspielen schlägt der Verzeichnis-Abgleich fehl | `APP_SECRET` ist nicht mehr dasselbe, und das Bind-Passwort ist unlesbar. Entweder den alten Wert eintragen und neu starten, oder unter **Admin → Verzeichnis** das Bind-Passwort neu setzen. |
|
||
| Nach dem Zurückspielen fehlen Tabellen oder Spalten | Der Abzug ist älter als der Code. `prisma migrate deploy` nachziehen, siehe Schritt 5 oben. |
|
||
| Die Anwendung startet nach dem Zurückspielen nicht | `docker compose logs web` beziehungsweise `journalctl -u laufzettel -n 50 --no-pager`. Meist läuft noch die Migration, oder sie ist gescheitert. |
|
||
| Ein Tag-Code taucht doppelt auf | Kann nach einem Zurückspielen nicht passieren; ein partieller Eindeutigkeitsindex in der Datenbank verhindert es. Siehst du es doch, ist der Abzug nicht sauber eingespielt — Datenbank leeren und noch einmal von vorn. |
|