Files
einkaufsapp/docs/betrieb.md
2026-08-08 20:31:58 +02:00

6.2 KiB
Raw Blame History

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

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

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:

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

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):

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

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

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.