175 lines
6.2 KiB
Markdown
175 lines
6.2 KiB
Markdown
# 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.
|