fix: Server-Daten in Named Volume statt Bind-Mount
./data war ein Bind-Mount relativ zum Git-Checkout - bei Dokploy wird das Repo pro Deploy frisch geklont, ein per CLI angelegter Nutzer war nach einem reinen Routing-Redeploy spurlos verschwunden. Betraf nicht nur Nutzer, sondern denselben Api:DataPath für Ereignis-Log/Snapshots/ Anhänge - also jeden Server-Datenbestand bei jedem Redeploy. - docker-compose.yml: Named Volume "api-data" statt Bind-Mount, lebt unabhängig vom Checkout im Docker-Daemon. - backup.sh: auf volume-basiertes Backup umgeschrieben (Alpine- Hilfscontainer statt direktem Host-Pfad). - docker/README.md, TODO.md 10.2.4: Vorfall dokumentiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
neuere SDK-Version gehoben, muss dieser Tag mitgezogen werden, sonst bricht der Docker-Build
|
||||||
auf die gleiche Art wieder ab.
|
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 Verschlüsselung
|
||||||
- [ ] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase).
|
- [ ] **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.
|
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
||||||
|
|||||||
+22
-10
@@ -7,8 +7,11 @@ cd docker
|
|||||||
JWT_SECRET=<zufälliger-langer-string> docker compose up -d
|
JWT_SECRET=<zufälliger-langer-string> docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
`./data` (relativ zu `docker/`) wird als Volume gemountet und enthält alle Server-Daten
|
Alle Server-Daten (Ereignis-Logs, Snapshots, Anhänge, Nutzer) liegen im Named Volume `api-data` –
|
||||||
(Ereignis-Logs, Snapshots, Nutzer) – bei Neustarts/Updates bleibt es erhalten.
|
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
|
## Nutzer anlegen
|
||||||
|
|
||||||
@@ -41,18 +44,27 @@ docker compose exec api dotnet LehrerApp.Api.dll set-password <benutzername> --p
|
|||||||
|
|
||||||
## Backup
|
## Backup
|
||||||
|
|
||||||
`backup.sh` sichert `./data` (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als komprimiertes
|
`backup.sh` sichert das Named Volume `api-data` (Ereignis-Logs, Snapshots, Anhänge, Nutzer) über
|
||||||
Tar-Archiv unter `docker/backups/` und räumt Archive älter als 30 Tage automatisch auf. Läuft
|
einen kurzlebigen Alpine-Container als komprimiertes Tar-Archiv unter `docker/backups/` und räumt
|
||||||
direkt auf dem Host (kein Container-Zugriff nötig, `./data` liegt dort per Bind-Mount ohnehin):
|
Archive älter als 30 Tage automatisch auf:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./docker/backup.sh
|
./docker/backup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Der volle Docker-Volume-Name ist `<compose-projektname>_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:
|
Für regelmäßige Sicherung z. B. per Cron, einmal täglich nachts:
|
||||||
|
|
||||||
```bash
|
```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
|
## 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
|
6. **Nutzer anlegen** nach dem ersten Deploy: über Dokploys Terminal/Exec-Tab am laufenden
|
||||||
Container `dotnet LehrerApp.Api.dll create-user <name>` ausführen (siehe oben).
|
Container `dotnet LehrerApp.Api.dll create-user <name>` ausführen (siehe oben).
|
||||||
|
|
||||||
Der `./data`-Bind-Mount bleibt unverändert – Dokploy hält das Checkout-Verzeichnis der
|
**Wichtig:** die Daten liegen im Named Volume `api-data`, nicht in einem Bind-Mount – Dokploy
|
||||||
Compose-App über Redeploys hinweg stabil, `backup.sh` funktioniert damit unverändert weiter.
|
klont das Repo bei jedem Redeploy neu, ein Bind-Mount relativ zum Checkout wäre dabei jedes Mal
|
||||||
Nach dem ersten echten Deploy einmal gegenprüfen: Redeploy anstoßen und verifizieren, dass
|
leer gewesen (das ist real passiert, siehe TODO.md 10.2.4). Named Volumes übersteht Dokploys
|
||||||
Daten (z. B. ein testweise angelegter Nutzer) erhalten bleiben.
|
`docker compose up --build --remove-orphans` dagegen unverändert.
|
||||||
|
|||||||
+21
-8
@@ -1,25 +1,38 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Sichert das komplette ./data-Verzeichnis (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als
|
# Sichert das Docker-Volume "api-data" (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als
|
||||||
# komprimiertes Tar-Archiv. Läuft außerhalb des Containers direkt auf dem Host, da ./data per
|
# komprimiertes Tar-Archiv. Läuft über einen kurzlebigen Alpine-Container mit Zugriff auf das
|
||||||
# Bind-Mount ohnehin dort liegt (siehe docker-compose.yml) — kein Zugriff auf den Container nötig.
|
# 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 "<compose-projektname>_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:
|
# 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
|
set -eu
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
DATA_DIR="$SCRIPT_DIR/data"
|
|
||||||
BACKUP_DIR="$SCRIPT_DIR/backups"
|
BACKUP_DIR="$SCRIPT_DIR/backups"
|
||||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
KEEP_DAYS=30
|
KEEP_DAYS=30
|
||||||
|
VOLUME_NAME="${LEHRERAPP_DATA_VOLUME:-docker_api-data}"
|
||||||
|
|
||||||
if [ ! -d "$DATA_DIR" ]; then
|
if ! docker volume inspect "$VOLUME_NAME" >/dev/null 2>&1; then
|
||||||
echo "Kein Datenverzeichnis unter $DATA_DIR gefunden - nichts zu sichern." >&2
|
echo "Docker-Volume '$VOLUME_NAME' nicht gefunden (docker volume ls | grep api-data) - nichts zu sichern." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
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.
|
# 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
|
find "$BACKUP_DIR" -name 'lehrerapp-data-*.tar.gz' -mtime "+$KEEP_DAYS" -delete
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
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:
|
environment:
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
api-data:
|
||||||
|
|||||||
Reference in New Issue
Block a user