diff --git a/TODO.md b/TODO.md index 03494b5..3c81508 100644 --- a/TODO.md +++ b/TODO.md @@ -1417,6 +1417,19 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba neuere SDK-Version gehoben, muss dieser Tag mitgezogen werden, sonst bricht der Docker-Build auf die gleiche Art wieder ab. + **Nachtrag 2 (Datenverlust bei Redeploy — ⚠️ war ein echter Vorfall):** `docker-compose.yml` + mountete `./data` (relativ zum Git-Checkout) als Bind-Mount. Dokploy klont das Repo bei + jedem Deploy frisch — ein per CLI angelegter Nutzer war nach einem reinen Routing-Redeploy + (keine Datenänderung beabsichtigt) spurlos verschwunden. Betraf nicht nur Nutzer, sondern + denselben `Api:DataPath` für Ereignis-Log/Snapshots/Anhänge — also potenziell **jeden** + Server-Datenbestand bei jedem Redeploy. Fix: `docker-compose.yml` auf ein Named Volume + (`api-data`) umgestellt, das unabhängig vom Git-Checkout im Docker-Daemon lebt und + `docker compose up --build --remove-orphans` übersteht. `docker/backup.sh` entsprechend auf + volume-basiertes Backup (Alpine-Hilfscontainer statt direktem Host-Pfad) umgeschrieben. + **Merke:** bei Git-basierten Deploy-Plattformen (Dokploy & vergleichbare) niemals Bind-Mounts + relativ zum Checkout-Verzeichnis für persistente Daten verwenden — nur Named Volumes oder ein + Pfad explizit außerhalb des von der Plattform verwalteten Checkouts sind sicher. + ### 10.3 Verschlüsselung - [ ] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase). - [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel. diff --git a/docker/README.md b/docker/README.md index 40064bb..f9f7872 100644 --- a/docker/README.md +++ b/docker/README.md @@ -7,8 +7,11 @@ cd docker JWT_SECRET= docker compose up -d ``` -`./data` (relativ zu `docker/`) wird als Volume gemountet und enthält alle Server-Daten -(Ereignis-Logs, Snapshots, Nutzer) – bei Neustarts/Updates bleibt es erhalten. +Alle Server-Daten (Ereignis-Logs, Snapshots, Anhänge, Nutzer) liegen im Named Volume `api-data` – +bei Neustarts/Updates bleibt es erhalten. Bewusst **kein** Bind-Mount auf einen Host-Pfad: bei +Git-basierten Deployments (z. B. Dokploy) wird das Checkout-Verzeichnis bei jedem Redeploy neu +geklont, ein dort liegendes `./data` wäre dabei jedes Mal leer (siehe TODO.md 10.2.4 für den +Vorfall, der das aufgedeckt hat). ## Nutzer anlegen @@ -41,18 +44,27 @@ docker compose exec api dotnet LehrerApp.Api.dll set-password --p ## Backup -`backup.sh` sichert `./data` (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als komprimiertes -Tar-Archiv unter `docker/backups/` und räumt Archive älter als 30 Tage automatisch auf. Läuft -direkt auf dem Host (kein Container-Zugriff nötig, `./data` liegt dort per Bind-Mount ohnehin): +`backup.sh` sichert das Named Volume `api-data` (Ereignis-Logs, Snapshots, Anhänge, Nutzer) über +einen kurzlebigen Alpine-Container als komprimiertes Tar-Archiv unter `docker/backups/` und räumt +Archive älter als 30 Tage automatisch auf: ```bash ./docker/backup.sh ``` +Der volle Docker-Volume-Name ist `_api-data`. Lokal (`cd docker && docker +compose up`) ist das `docker_api-data` (Default im Skript), bei Dokploy der App-Slug, z. B. +`lehrerapp-syncserver-xyz_api-data` – im Zweifel prüfen mit `docker volume ls | grep api-data` +und per Umgebungsvariable übergeben: + +```bash +LEHRERAPP_DATA_VOLUME=lehrerapp-syncserver-xyz_api-data ./docker/backup.sh +``` + Für regelmäßige Sicherung z. B. per Cron, einmal täglich nachts: ```bash -0 3 * * * /pfad/zu/docker/backup.sh >> /pfad/zu/docker/backup.log 2>&1 +0 3 * * * LEHRERAPP_DATA_VOLUME=... /pfad/zu/docker/backup.sh >> /pfad/zu/docker/backup.log 2>&1 ``` ## Rate Limiting @@ -82,7 +94,7 @@ selbst aus `docker/Dockerfile.api`. 6. **Nutzer anlegen** nach dem ersten Deploy: über Dokploys Terminal/Exec-Tab am laufenden Container `dotnet LehrerApp.Api.dll create-user ` ausführen (siehe oben). -Der `./data`-Bind-Mount bleibt unverändert – Dokploy hält das Checkout-Verzeichnis der -Compose-App über Redeploys hinweg stabil, `backup.sh` funktioniert damit unverändert weiter. -Nach dem ersten echten Deploy einmal gegenprüfen: Redeploy anstoßen und verifizieren, dass -Daten (z. B. ein testweise angelegter Nutzer) erhalten bleiben. +**Wichtig:** die Daten liegen im Named Volume `api-data`, nicht in einem Bind-Mount – Dokploy +klont das Repo bei jedem Redeploy neu, ein Bind-Mount relativ zum Checkout wäre dabei jedes Mal +leer gewesen (das ist real passiert, siehe TODO.md 10.2.4). Named Volumes übersteht Dokploys +`docker compose up --build --remove-orphans` dagegen unverändert. diff --git a/docker/backup.sh b/docker/backup.sh index e264fc3..e2f7145 100755 --- a/docker/backup.sh +++ b/docker/backup.sh @@ -1,25 +1,38 @@ #!/bin/sh -# Sichert das komplette ./data-Verzeichnis (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als -# komprimiertes Tar-Archiv. Läuft außerhalb des Containers direkt auf dem Host, da ./data per -# Bind-Mount ohnehin dort liegt (siehe docker-compose.yml) — kein Zugriff auf den Container nötig. +# Sichert das Docker-Volume "api-data" (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als +# komprimiertes Tar-Archiv. Läuft über einen kurzlebigen Alpine-Container mit Zugriff auf das +# Volume - kein direkter Host-Pfad, seit das Volume kein Bind-Mount mehr ist (siehe +# docker-compose.yml: Dokploy-Redeploys klonen das Repo neu, ein Bind-Mount relativ zum +# Checkout würde dabei geleert, siehe TODO.md 10.2.4). +# +# Der volle Docker-Volume-Name ist "_api-data" - bei einem lokalen +# "cd docker && docker compose up" ist das "docker_api-data" (Default unten), bei Dokploy der +# App-Slug, z.B. "lehrerapp-syncserver-xyz_api-data". Im Zweifel prüfen mit: +# docker volume ls | grep api-data +# und den echten Namen per LEHRERAPP_DATA_VOLUME übergeben: +# LEHRERAPP_DATA_VOLUME=lehrerapp-syncserver-xyz_api-data /pfad/zu/docker/backup.sh # # Aufruf z.B. per Cron: -# 0 3 * * * /pfad/zu/docker/backup.sh >> /pfad/zu/docker/backup.log 2>&1 +# 0 3 * * * LEHRERAPP_DATA_VOLUME=... /pfad/zu/docker/backup.sh >> /pfad/zu/docker/backup.log 2>&1 set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -DATA_DIR="$SCRIPT_DIR/data" BACKUP_DIR="$SCRIPT_DIR/backups" TIMESTAMP="$(date +%Y%m%d-%H%M%S)" KEEP_DAYS=30 +VOLUME_NAME="${LEHRERAPP_DATA_VOLUME:-docker_api-data}" -if [ ! -d "$DATA_DIR" ]; then - echo "Kein Datenverzeichnis unter $DATA_DIR gefunden - nichts zu sichern." >&2 +if ! docker volume inspect "$VOLUME_NAME" >/dev/null 2>&1; then + echo "Docker-Volume '$VOLUME_NAME' nicht gefunden (docker volume ls | grep api-data) - nichts zu sichern." >&2 exit 1 fi mkdir -p "$BACKUP_DIR" -tar -czf "$BACKUP_DIR/lehrerapp-data-$TIMESTAMP.tar.gz" -C "$SCRIPT_DIR" data +docker run --rm \ + -v "$VOLUME_NAME":/data:ro \ + -v "$BACKUP_DIR":/backup \ + alpine:3 \ + tar -czf "/backup/lehrerapp-data-$TIMESTAMP.tar.gz" -C /data . # Alte Backups jenseits von KEEP_DAYS aufräumen, damit das Verzeichnis nicht unbegrenzt wächst. find "$BACKUP_DIR" -name 'lehrerapp-data-*.tar.gz' -mtime "+$KEEP_DAYS" -delete diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c4fa666..e09aefe 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,8 +6,14 @@ services: ports: - "5000:5000" volumes: - - ./data:/app/data + # Named Volume statt Bind-Mount: Plattformen wie Dokploy klonen das Repo bei jedem Deploy + # neu, ein Bind-Mount relativ zum Checkout (./data) würde dabei geleert. Das Named Volume + # lebt im Docker-Daemon und übersteht "docker compose up --build --remove-orphans". + - api-data:/app/data environment: - JWT_SECRET=${JWT_SECRET} - ASPNETCORE_ENVIRONMENT=Production restart: unless-stopped + +volumes: + api-data: