diff --git a/.env.example b/.env.example index c9ef67e..e1d5da8 100644 --- a/.env.example +++ b/.env.example @@ -32,3 +32,9 @@ NODE_ENV=development # Gitea Registry Login für Watchtower (automatische Image-Updates) GITEA_REGISTRY_USER=adminpepe 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 diff --git a/deploy/portainer-stack.yml b/deploy/portainer-stack.yml index 422fbba..ba12322 100644 --- a/deploy/portainer-stack.yml +++ b/deploy/portainer-stack.yml @@ -86,11 +86,57 @@ services: networks: - 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: postgres_data: driver: local minio_data: driver: local + backups_data: + driver: local networks: lageplan-net: diff --git a/docker-compose.yml b/docker-compose.yml index 6e16046..01780a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,49 @@ services: volumes: - ./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: postgres_data: minio_data: + backups_data: diff --git a/docs/BACKUP.md b/docs/BACKUP.md new file mode 100644 index 0000000..5974d15 --- /dev/null +++ b/docs/BACKUP.md @@ -0,0 +1,82 @@ +# Backup & Restore — Lageplan + +Automatisches Backup als eigener Container (`backup`). Sichert **täglich**: +- **Datenbank** (PostgreSQL) → `db_.dump` (pg_dump, custom/komprimiert) +- **Hochgeladene Dateien** (MinIO: Pläne, Logos, Symbole) → `files_.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 -d /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. diff --git a/docs/legal/technical-organizational-measures.md b/docs/legal/technical-organizational-measures.md index 9573d67..007bcd7 100644 --- a/docs/legal/technical-organizational-measures.md +++ b/docs/legal/technical-organizational-measures.md @@ -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**. ## Backup & Wiederherstellung -- ⚠️ Backups: `[VORHANDEN? FREQUENZ? AUFBEWAHRUNG?]` (siehe `docs/backup-und-seeding.md`). -- ❌ Restore-Tests: `[DOKUMENTIEREN]`. +- ✅ Automatisches Backup (eigener `backup`-Container): täglich Postgres-Dump + MinIO-Dateien, + 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 - ✅ Server-Logs (console). ⚠️ Vermeidung sensibler Daten in Logs → **Review empfohlen**. diff --git a/package.json b/package.json index 16774fb..3be4652 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lageplan", - "version": "1.8.10", + "version": "1.8.11", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "private": true, "scripts": { diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..d95b25a --- /dev/null +++ b/scripts/restore.sh @@ -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 " + 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"