feat(ops): Automatisches Backup (DB + Dateien, 30 Tage) (v1.8.11)
Schliesst den offenen Punkt: die Datenschutzerklärung nannte "bis zu 30 Tage", real fehlte ein Backup. - Neuer backup-Container (postgres:16-alpine) in docker-compose.yml und deploy/portainer-stack.yml: täglich pg_dump (custom/komprimiert) + tar der MinIO-Dateien, Prune > 30 Tage, im Volume backups_data - Konfigurierbar: BACKUP_RETENTION_DAYS, BACKUP_INTERVAL_HOURS (.env.example ergänzt) - scripts/restore.sh + docs/BACKUP.md (Einrichtung, Off-Site-Hinweis, Restore DB+Dateien, Restore-Test-Tabelle) - TOM-Doku aktualisiert (Backup vorhanden) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,3 +32,9 @@ NODE_ENV=development
|
|||||||
# Gitea Registry Login für Watchtower (automatische Image-Updates)
|
# Gitea Registry Login für Watchtower (automatische Image-Updates)
|
||||||
GITEA_REGISTRY_USER=adminpepe
|
GITEA_REGISTRY_USER=adminpepe
|
||||||
GITEA_REGISTRY_PASS=dein_gitea_token_oder_passwort
|
GITEA_REGISTRY_PASS=dein_gitea_token_oder_passwort
|
||||||
|
|
||||||
|
# --- Backup (automatischer DB- + Datei-Backup-Container) ---
|
||||||
|
# Aufbewahrungsdauer in Tagen (muss zur Datenschutzerklaerung passen)
|
||||||
|
BACKUP_RETENTION_DAYS=30
|
||||||
|
# Intervall in Stunden (24 = taeglich)
|
||||||
|
BACKUP_INTERVAL_HOURS=24
|
||||||
|
|||||||
@@ -86,11 +86,57 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- lageplan-net
|
- lageplan-net
|
||||||
|
|
||||||
|
# --- Automatisches Backup: täglicher DB-Dump + Datei-Backup (30 Tage) ---
|
||||||
|
# Backups im Volume "backups_data". Für Off-Site auf einen NAS-Pfad umstellen, z.B.:
|
||||||
|
# volumes: - /volume1/backups/lageplan:/backups
|
||||||
|
backup:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PGHOST: db
|
||||||
|
PGUSER: ${POSTGRES_USER}
|
||||||
|
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
PGDATABASE: ${POSTGRES_DB}
|
||||||
|
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-30}
|
||||||
|
BACKUP_INTERVAL_HOURS: ${BACKUP_INTERVAL_HOURS:-24}
|
||||||
|
volumes:
|
||||||
|
- backups_data:/backups
|
||||||
|
- minio_data:/minio:ro
|
||||||
|
networks:
|
||||||
|
- lageplan-net
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
echo "[backup] Start - alle $${BACKUP_INTERVAL_HOURS}h, Aufbewahrung $${BACKUP_RETENTION_DAYS} Tage";
|
||||||
|
while true; do
|
||||||
|
TS=$$(date +%Y-%m-%d_%H%M%S);
|
||||||
|
mkdir -p /backups;
|
||||||
|
echo "[backup] $$TS Datenbank sichern...";
|
||||||
|
if pg_dump -h "$$PGHOST" -U "$$PGUSER" -d "$$PGDATABASE" -Fc -f "/backups/db_$$TS.dump"; then
|
||||||
|
echo "[backup] DB ok: db_$$TS.dump";
|
||||||
|
else
|
||||||
|
echo "[backup] DB FEHLGESCHLAGEN"; rm -f "/backups/db_$$TS.dump";
|
||||||
|
fi;
|
||||||
|
if [ -d /minio ]; then
|
||||||
|
tar czf "/backups/files_$$TS.tar.gz" -C /minio . 2>/dev/null && echo "[backup] Dateien ok: files_$$TS.tar.gz" || echo "[backup] Dateien uebersprungen";
|
||||||
|
fi;
|
||||||
|
find /backups -name "db_*.dump" -mtime +$$BACKUP_RETENTION_DAYS -delete 2>/dev/null || true;
|
||||||
|
find /backups -name "files_*.tar.gz" -mtime +$$BACKUP_RETENTION_DAYS -delete 2>/dev/null || true;
|
||||||
|
echo "[backup] fertig - naechstes in $${BACKUP_INTERVAL_HOURS}h";
|
||||||
|
sleep $$(( BACKUP_INTERVAL_HOURS * 3600 ));
|
||||||
|
done
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
driver: local
|
driver: local
|
||||||
minio_data:
|
minio_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
backups_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
lageplan-net:
|
lageplan-net:
|
||||||
|
|||||||
@@ -84,6 +84,49 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./prisma:/app/prisma
|
- ./prisma:/app/prisma
|
||||||
|
|
||||||
|
# Automatisches Backup: täglicher Postgres-Dump + Datei-Backup (MinIO), 30 Tage Aufbewahrung.
|
||||||
|
# Backups liegen im Volume "backups_data" (für Off-Site: unten auf einen Host-Pfad umstellen).
|
||||||
|
backup:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: lageplan-backup
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PGHOST: db
|
||||||
|
PGUSER: ${POSTGRES_USER:-lageplan}
|
||||||
|
PGPASSWORD: ${POSTGRES_PASSWORD:-lageplan_secret}
|
||||||
|
PGDATABASE: ${POSTGRES_DB:-lageplan}
|
||||||
|
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-30}
|
||||||
|
BACKUP_INTERVAL_HOURS: ${BACKUP_INTERVAL_HOURS:-24}
|
||||||
|
volumes:
|
||||||
|
- backups_data:/backups
|
||||||
|
- minio_data:/minio:ro
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
echo "[backup] Start - alle $${BACKUP_INTERVAL_HOURS}h, Aufbewahrung $${BACKUP_RETENTION_DAYS} Tage";
|
||||||
|
while true; do
|
||||||
|
TS=$$(date +%Y-%m-%d_%H%M%S);
|
||||||
|
mkdir -p /backups;
|
||||||
|
echo "[backup] $$TS Datenbank sichern...";
|
||||||
|
if pg_dump -h "$$PGHOST" -U "$$PGUSER" -d "$$PGDATABASE" -Fc -f "/backups/db_$$TS.dump"; then
|
||||||
|
echo "[backup] DB ok: db_$$TS.dump";
|
||||||
|
else
|
||||||
|
echo "[backup] DB FEHLGESCHLAGEN"; rm -f "/backups/db_$$TS.dump";
|
||||||
|
fi;
|
||||||
|
if [ -d /minio ]; then
|
||||||
|
tar czf "/backups/files_$$TS.tar.gz" -C /minio . 2>/dev/null && echo "[backup] Dateien ok: files_$$TS.tar.gz" || echo "[backup] Dateien uebersprungen";
|
||||||
|
fi;
|
||||||
|
find /backups -name "db_*.dump" -mtime +$$BACKUP_RETENTION_DAYS -delete 2>/dev/null || true;
|
||||||
|
find /backups -name "files_*.tar.gz" -mtime +$$BACKUP_RETENTION_DAYS -delete 2>/dev/null || true;
|
||||||
|
echo "[backup] fertig - naechstes in $${BACKUP_INTERVAL_HOURS}h";
|
||||||
|
sleep $$(( BACKUP_INTERVAL_HOURS * 3600 ));
|
||||||
|
done
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
|
backups_data:
|
||||||
|
|||||||
82
docs/BACKUP.md
Normal file
82
docs/BACKUP.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Backup & Restore — Lageplan
|
||||||
|
|
||||||
|
Automatisches Backup als eigener Container (`backup`). Sichert **täglich**:
|
||||||
|
- **Datenbank** (PostgreSQL) → `db_<zeitstempel>.dump` (pg_dump, custom/komprimiert)
|
||||||
|
- **Hochgeladene Dateien** (MinIO: Pläne, Logos, Symbole) → `files_<zeitstempel>.tar.gz`
|
||||||
|
|
||||||
|
**Aufbewahrung:** 30 Tage (ältere werden automatisch gelöscht) — passt zur Datenschutzerklärung.
|
||||||
|
Einstellbar über `BACKUP_RETENTION_DAYS` und `BACKUP_INTERVAL_HOURS` (siehe `.env.example`).
|
||||||
|
|
||||||
|
Die Backups liegen im Docker-Volume **`backups_data`** (Pfad im Container: `/backups`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Einrichtung
|
||||||
|
|
||||||
|
Der `backup`-Service ist in `docker-compose.yml` und `deploy/portainer-stack.yml` enthalten.
|
||||||
|
Nach dem nächsten Stack-Update (Portainer: Stack neu deployen) läuft er automatisch.
|
||||||
|
|
||||||
|
Prüfen, dass er läuft und sichert:
|
||||||
|
```
|
||||||
|
docker logs lageplan-backup # oder: docker compose logs backup
|
||||||
|
docker compose exec backup ls -lh /backups
|
||||||
|
```
|
||||||
|
Beim ersten Start wird sofort ein Backup erstellt, danach alle 24 h.
|
||||||
|
|
||||||
|
### Empfehlung: Backups aus dem Container heraus auf den NAS legen (Off-Site)
|
||||||
|
Standardmässig liegen die Backups in einem Docker-Volume. Damit sie ausserhalb von Docker
|
||||||
|
(z. B. auf einer NAS-Freigabe, die du separat sicherst) landen, im Stack den `backup`-Service auf
|
||||||
|
einen **Host-Pfad** umstellen:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /volume1/backups/lageplan:/backups # statt backups_data:/backups
|
||||||
|
- minio_data:/minio:ro
|
||||||
|
```
|
||||||
|
So kannst du die Dateien einfach kopieren/auf ein anderes Medium spiegeln. **Wichtig:** Ein Backup
|
||||||
|
auf demselben Gerät wie die Daten schützt nicht vor Geräteausfall/Diebstahl — kopiere die Backups
|
||||||
|
regelmässig an einen zweiten Ort.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wiederherstellen (Restore)
|
||||||
|
|
||||||
|
> **Destruktiv.** Vorher die Web-App stoppen (`docker stop lageplan-web`) und sicherstellen, dass
|
||||||
|
> niemand arbeitet.
|
||||||
|
|
||||||
|
### 1) Datenbank
|
||||||
|
```
|
||||||
|
# Verfügbare Dumps anzeigen
|
||||||
|
docker compose exec backup ls -lh /backups
|
||||||
|
|
||||||
|
# Wiederherstellen (Dateiname einsetzen)
|
||||||
|
docker compose exec backup pg_restore --clean --if-exists --no-owner \
|
||||||
|
-h db -U <POSTGRES_USER> -d <POSTGRES_DB> /backups/db_2026-07-23_030000.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Dateien (Uploads/MinIO)
|
||||||
|
Da `/minio` im backup-Container schreibgeschützt ist, den Tarball in einen Container mit
|
||||||
|
Schreibzugriff auf das MinIO-Volume entpacken:
|
||||||
|
```
|
||||||
|
docker run --rm -v minio_data:/data -v backups_data:/backups alpine \
|
||||||
|
sh -c "tar xzf /backups/files_2026-07-23_030000.tar.gz -C /data"
|
||||||
|
```
|
||||||
|
Danach Web-App wieder starten: `docker start lageplan-web`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Restore-Test (mindestens 1× durchführen und hier notieren)
|
||||||
|
|
||||||
|
Ein Backup ist erst dann ein Backup, wenn ein Restore nachweislich funktioniert.
|
||||||
|
Empfehlung: **halbjährlich** auf einer Testumgebung durchspielen.
|
||||||
|
|
||||||
|
| Datum | Getestet von | Ergebnis | Bemerkung |
|
||||||
|
|-------|--------------|----------|-----------|
|
||||||
|
| | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was ist NICHT im Backup?
|
||||||
|
- Zahlungsdaten liegen bei Stripe (nicht in der App).
|
||||||
|
- E-Mails liegen beim SMTP-Anbieter.
|
||||||
|
- Secrets (`NEXTAUTH_SECRET`, MinIO-Keys) — separat sicher aufbewahren (Passwort-Manager),
|
||||||
|
sie stehen nicht in den Backups.
|
||||||
@@ -36,8 +36,10 @@ Legende: ✅ vorhanden · ⚠️ teilweise / zu bestätigen · ❌ offen / nicht
|
|||||||
- ⚠️ Secrets-Verwaltung: ENV (`NEXTAUTH_SECRET`, MinIO) + DB (`SystemSetting`: SMTP/Stripe) → **Absicherung der DB-Secrets bestätigen**.
|
- ⚠️ Secrets-Verwaltung: ENV (`NEXTAUTH_SECRET`, MinIO) + DB (`SystemSetting`: SMTP/Stripe) → **Absicherung der DB-Secrets bestätigen**.
|
||||||
|
|
||||||
## Backup & Wiederherstellung
|
## Backup & Wiederherstellung
|
||||||
- ⚠️ Backups: `[VORHANDEN? FREQUENZ? AUFBEWAHRUNG?]` (siehe `docs/backup-und-seeding.md`).
|
- ✅ Automatisches Backup (eigener `backup`-Container): täglich Postgres-Dump + MinIO-Dateien,
|
||||||
- ❌ Restore-Tests: `[DOKUMENTIEREN]`.
|
30 Tage Aufbewahrung (`docker-compose.yml` / `deploy/portainer-stack.yml`, Anleitung `docs/BACKUP.md`).
|
||||||
|
- ⚠️ Off-Site-Kopie: empfohlen (Backups zusätzlich an einen zweiten Ort spiegeln) — siehe `docs/BACKUP.md`.
|
||||||
|
- ⚠️ Restore-Test: mindestens 1× durchführen und in `docs/BACKUP.md` protokollieren.
|
||||||
|
|
||||||
## Logging & Monitoring
|
## Logging & Monitoring
|
||||||
- ✅ Server-Logs (console). ⚠️ Vermeidung sensibler Daten in Logs → **Review empfohlen**.
|
- ✅ Server-Logs (console). ⚠️ Vermeidung sensibler Daten in Logs → **Review empfohlen**.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lageplan",
|
"name": "lageplan",
|
||||||
"version": "1.8.10",
|
"version": "1.8.11",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
44
scripts/restore.sh
Normal file
44
scripts/restore.sh
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# =============================================================
|
||||||
|
# Lageplan – Restore-Helfer
|
||||||
|
# =============================================================
|
||||||
|
# Stellt einen Datenbank-Dump (und optional die Dateien) wieder her.
|
||||||
|
# ACHTUNG: Der DB-Restore ist DESTRUKTIV – bestehende Daten werden ersetzt.
|
||||||
|
# Vorher die Web-App stoppen und sicherstellen, dass niemand arbeitet.
|
||||||
|
#
|
||||||
|
# Verwendung (auf dem Server, im Verzeichnis mit der docker-compose.yml):
|
||||||
|
#
|
||||||
|
# 1) Verfügbare Backups anzeigen:
|
||||||
|
# docker compose exec backup ls -lh /backups
|
||||||
|
#
|
||||||
|
# 2) Datenbank wiederherstellen (Dateiname aus Schritt 1):
|
||||||
|
# docker compose exec backup sh /scripts/restore.sh db_2026-07-23_030000.dump
|
||||||
|
# (funktioniert nur, wenn dieses Verzeichnis in den backup-Container gemountet ist;
|
||||||
|
# sonst den Befehl aus dem Abschnitt "Ohne Skript" in docs/BACKUP.md nutzen.)
|
||||||
|
#
|
||||||
|
# 3) Dateien (Uploads) wiederherstellen: siehe docs/BACKUP.md
|
||||||
|
# =============================================================
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DUMP="${1:-}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/backups}"
|
||||||
|
PGHOST="${PGHOST:-db}"
|
||||||
|
PGUSER="${PGUSER:-lageplan}"
|
||||||
|
PGDATABASE="${PGDATABASE:-lageplan}"
|
||||||
|
|
||||||
|
if [ -z "$DUMP" ]; then
|
||||||
|
echo "Verwendung: restore.sh <db_dumpfile>"
|
||||||
|
echo "Verfuegbare Dumps in $BACKUP_DIR:"
|
||||||
|
ls -1 "$BACKUP_DIR"/db_*.dump 2>/dev/null || echo " (keine gefunden)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FILE="$BACKUP_DIR/$DUMP"
|
||||||
|
[ -f "$FILE" ] || { echo "Nicht gefunden: $FILE"; exit 1; }
|
||||||
|
|
||||||
|
echo "!! DESTRUKTIV: Stelle '$DUMP' in Datenbank '$PGDATABASE' auf Host '$PGHOST' wieder her."
|
||||||
|
echo " Bestehende Daten werden ersetzt. Abbruch mit Ctrl+C, weiter mit Enter."
|
||||||
|
read -r _
|
||||||
|
|
||||||
|
pg_restore --clean --if-exists --no-owner -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" "$FILE"
|
||||||
|
echo "[restore] Datenbank wiederhergestellt aus $DUMP"
|
||||||
Reference in New Issue
Block a user