Erste Produktivversion
This commit is contained in:
174
docs/betrieb.md
Normal file
174
docs/betrieb.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Betrieb: Sicherung, Wiederherstellung, Aufräumen
|
||||
|
||||
## Was gesichert werden muss
|
||||
|
||||
| Was | Wo | Ohne das … |
|
||||
|---|---|---|
|
||||
| Datenbank | Volume `einkaufsapp_db_data` | ist alles weg |
|
||||
| `.env` | Projektverzeichnis | Sessions ungültig, Push-Anmeldungen tot, DB-Zugang verloren |
|
||||
| Projektverzeichnis | Quelltext | neu aus dem Archiv holen – kein Datenverlust |
|
||||
|
||||
Der Quelltext gehört ins Versionsverwaltungssystem, die `.env` ausdrücklich
|
||||
**nicht**: Sie enthält Datenbankpasswort, `SECRET_KEY`, SMTP-Zugangsdaten und
|
||||
den privaten VAPID-Schlüssel. Sie gehört in den Passwortmanager oder in eine
|
||||
verschlüsselte Sicherung.
|
||||
|
||||
Warum die `.env` so wichtig ist: Ein verlorener `SECRET_KEY` ist verschmerzbar
|
||||
(alle müssen sich neu anmelden), ein verlorener `VAPID_PRIVATE_KEY` auch (alle
|
||||
müssen Push neu einschalten). Ein verlorenes `DB_PASSWORD` bei erhaltener
|
||||
Datenbank ist dagegen ärgerlich – dann kommt man an die eigenen Daten nur noch
|
||||
über das Root-Passwort heran.
|
||||
|
||||
## Datenbanksicherung
|
||||
|
||||
```bash
|
||||
cd /opt/einkaufsapp
|
||||
|
||||
# Konsistenter Auszug ohne Anhalten der Anwendung
|
||||
docker compose exec -T db mariadb-dump \
|
||||
-u root -p"$(grep '^DB_ROOT_PASSWORD=' .env | cut -d= -f2-)" \
|
||||
--single-transaction --quick --routines --events \
|
||||
--default-character-set=utf8mb4 \
|
||||
einkaufsapp | gzip > "einkaufsapp-$(date +%F).sql.gz"
|
||||
```
|
||||
|
||||
`--single-transaction` ist der entscheidende Schalter: Er nimmt einen
|
||||
konsistenten Stand aus einem Zeitpunkt, ohne die Tabellen zu sperren. Ohne ihn
|
||||
kann eine Sicherung mitten in einer Änderung entstehen – zum Beispiel mit einem
|
||||
Listeneintrag, dessen Artikel noch fehlt.
|
||||
|
||||
`--quick` verhindert, dass große Tabellen komplett in den Arbeitsspeicher
|
||||
geladen werden.
|
||||
|
||||
### Als tägliche Aufgabe
|
||||
|
||||
```bash
|
||||
sudo tee /etc/cron.daily/einkaufsapp-backup >/dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
cd /opt/einkaufsapp
|
||||
PASS=$(grep '^DB_ROOT_PASSWORD=' .env | cut -d= -f2-)
|
||||
DEST=/var/backups/einkaufsapp
|
||||
mkdir -p "$DEST"
|
||||
docker compose exec -T db mariadb-dump -u root -p"$PASS" \
|
||||
--single-transaction --quick --routines --events \
|
||||
--default-character-set=utf8mb4 einkaufsapp \
|
||||
| gzip > "$DEST/db-$(date +%F).sql.gz"
|
||||
# Vierzehn Tage aufbewahren
|
||||
find "$DEST" -name 'db-*.sql.gz' -mtime +14 -delete
|
||||
EOF
|
||||
sudo chmod +x /etc/cron.daily/einkaufsapp-backup
|
||||
```
|
||||
|
||||
Der Auszug landet damit im Dateisystem und wird von deiner vorhandenen
|
||||
Borgmatic-Sicherung mit erfasst. Das ist der bessere Weg, als das Volume direkt
|
||||
zu sichern: Ein Dateisystemabbild einer laufenden Datenbank ist nicht
|
||||
zuverlässig wiederherstellbar.
|
||||
|
||||
**Prüfen, dass die Sicherung etwas taugt:**
|
||||
|
||||
```bash
|
||||
zcat /var/backups/einkaufsapp/db-$(date +%F).sql.gz | tail -5
|
||||
```
|
||||
|
||||
Am Ende muss `-- Dump completed on …` stehen. Fehlt die Zeile, ist der Auszug
|
||||
abgebrochen – eine abgeschnittene Sicherung sieht sonst genauso aus wie eine
|
||||
vollständige.
|
||||
|
||||
## Wiederherstellung
|
||||
|
||||
```bash
|
||||
cd /opt/einkaufsapp
|
||||
docker compose down
|
||||
docker volume rm einkaufsapp_db_data # ACHTUNG: löscht den aktuellen Stand
|
||||
docker compose up -d db
|
||||
sleep 20 # MariaDB initialisiert sich
|
||||
|
||||
zcat einkaufsapp-2026-08-08.sql.gz | docker compose exec -T db \
|
||||
mariadb -u root -p"$(grep '^DB_ROOT_PASSWORD=' .env | cut -d= -f2-)" einkaufsapp
|
||||
|
||||
docker compose up -d
|
||||
docker compose logs --tail=20 api
|
||||
```
|
||||
|
||||
Alembic bringt das Schema beim Start auf den neuesten Stand, falls die
|
||||
Sicherung aus einer älteren Fassung stammt. Der umgekehrte Fall – neuere
|
||||
Sicherung, ältere Anwendung – geht nicht; dann erst den Quelltext aktualisieren.
|
||||
|
||||
**Einmal im Jahr ausprobieren.** Eine Sicherung, die nie zurückgespielt wurde,
|
||||
ist eine Vermutung, keine Sicherung.
|
||||
|
||||
## Aufräumen
|
||||
|
||||
Läuft von selbst: einmal 30 Sekunden nach dem Start des `api`-Containers,
|
||||
danach alle `CLEANUP_INTERVAL_HOURS` (Voreinstellung 24).
|
||||
|
||||
| Was | Frist | Einstellung |
|
||||
|---|---|---|
|
||||
| Weich gelöschte Listen, Einträge, Artikel, Märkte, Warengruppen | 30 Tage | `CLEANUP_DELETED_DAYS` |
|
||||
| Outbox-Quittungen | 7 Tage | `CLEANUP_OPS_DAYS` |
|
||||
| Abgelaufene Sitzungen | sofort | – |
|
||||
| Rate-Limit-Zähler | 1 Tag | – |
|
||||
| Verbrauchte Mail-Token | 7 Tage nach Ablauf | – |
|
||||
| Abgelaufene Einladungen und öffentliche Links | 30 Tage nach Ablauf | `CLEANUP_DELETED_DAYS` |
|
||||
| Produktzwischenspeicher | mindestens 1 Jahr | – |
|
||||
|
||||
Manuell auslösen (als Administrator angemeldet):
|
||||
|
||||
```bash
|
||||
curl -sS -b cookies.txt -H "X-CSRF-Token: $CSRF" \
|
||||
-X POST https://einkauf.example.de/api/admin/cleanup
|
||||
```
|
||||
|
||||
Im Log erscheint nach jedem Durchlauf eine Zeile wie
|
||||
`Aufräumen: 12 gelöschte Einträge, 340 Outbox-Quittungen`.
|
||||
|
||||
**Warum die 30 Tage nicht kürzer sein sollten:** So lange kann ein Gerät
|
||||
offline bleiben und beim nächsten Abgleich noch erfahren, dass eine Liste
|
||||
gelöscht wurde. Wird früher aufgeräumt, taucht die Liste auf dem Gerät weiter
|
||||
auf, bis jemand sie von Hand entfernt.
|
||||
|
||||
## Aktualisieren
|
||||
|
||||
```bash
|
||||
cd /opt/einkaufsapp
|
||||
# Sicherung ZUERST - Migrationen lassen sich nicht immer zurücknehmen
|
||||
/etc/cron.daily/einkaufsapp-backup
|
||||
|
||||
# Neuen Stand einspielen, dann:
|
||||
./tools/check-env.sh .env
|
||||
node tools/check-js.mjs
|
||||
python3 tools/check-nginx.py web
|
||||
|
||||
docker compose up -d --build
|
||||
docker compose logs --tail=30 api # Migrationen beobachten
|
||||
```
|
||||
|
||||
Nach Änderungen an den Dateien unter `web/html/js/` muss `VERSION` in
|
||||
`web/html/sw.js` hochgezählt werden – sonst behalten bereits installierte
|
||||
Clients die alte Fassung.
|
||||
|
||||
**`docker compose up -d`, nicht `restart`:** Ein Neustart übernimmt keine
|
||||
geänderten Werte aus der `.env`. Das hat in diesem Projekt schon mehrfach für
|
||||
Verwirrung gesorgt.
|
||||
|
||||
## Speicherplatz im Blick behalten
|
||||
|
||||
```bash
|
||||
docker compose exec db mariadb -u root -p einkaufsapp -e "
|
||||
SELECT table_name AS Tabelle,
|
||||
ROUND(data_length/1024/1024, 1) AS 'Daten MB',
|
||||
ROUND(index_length/1024/1024, 1) AS 'Index MB',
|
||||
table_rows AS 'Zeilen (geschätzt)'
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'einkaufsapp'
|
||||
ORDER BY data_length DESC;"
|
||||
```
|
||||
|
||||
Erwartungsgemäß wachsen `price_point` (jeder erfasste Preis) und
|
||||
`product_cache` (jeder gescannte Strichcode) am stärksten. Beide sind gewollt:
|
||||
Die Preisdatenbank ist der Zweck, und der Zwischenspeicher verhindert, dass
|
||||
jeder Scan nach draußen geht.
|
||||
|
||||
Wächst `applied_op` unerwartet, liegt ein Gerät mit einer festhängenden Outbox
|
||||
vor – dann lohnt ein Blick ins Log des `api`-Containers.
|
||||
Reference in New Issue
Block a user