14 KiB
Upgrade einer bestehenden Produktivinstallation
Diese Anleitung betrifft den ursprünglichen Stand mit den Tabellen User, Task, Comment, File und den Rollen ADMIN, VORGESETZTER, PFLEGER, BEARBEITER. Ausgangsschema: prisma/migrations/20251111164012_init/migration.sql, ursprünglicher Commit c2e00ff13a90041c43bdbdbebff51362f557ac40.
Empfohlener Weg: konsistente Sicherung der gestoppten Altanwendung in eine separate neue Installation wiederherstellen, dort migrieren, prüfen und erst dann umschalten. Das alte Datenbank- und Upload-Volume bleibt unangetastet. Eine frühere Probe mit einer Backupkopie ersetzt nicht die erneute Sicherung im eigentlichen Wartungsfenster.
Befehle sind Bash-Beispiele für den ursprünglichen Docker-Betrieb. Container-, Projekt-, DB- und Volume-Namen vor Ausführung mit der eigenen Installation vergleichen. Nicht ungeprüft aus einem anderen Checkout ausführen. Jeder nicht erfolgreiche Schritt beendet den Upgradeversuch; keine Freigabe bei Warnungen über fehlende Daten.
1. Quelle aufnehmen und Probe vorbereiten
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker inspect taskmanager-db --format '{{json .Mounts}}'
docker inspect taskmanager-app --format '{{json .Mounts}}'
Beim Original-Compose heißen Container taskmanager-db und taskmanager-app. Tatsächliche Volume-Namen hängen vom Compose-Projektnamen ab. Keine Annahme, dass ein Volume einfach uploads heißt. Den Mount für /app/public/uploads und den für PostgreSQL notieren. Bei Bind-Mounts stattdessen deren Verzeichnisse sichern. Bei nicht persistenten Uploads zuerst das gesamte /app/public/uploads aus dem alten Container sichern; Container nicht löschen.
Festhalten: laufender Git-Commit/Image-ID, Compose-Konfiguration, verschlüsselte Kopie der Secrets, DB-Version, Benutzername/DB-Name, Proxy-Routing, Zeitzone und Mengen der vier Tabellen. App-spezifische lokale Änderungen am Schema mit dem genannten Ausgangsschema vergleichen. Unbekannte Fork-Schemata benötigen eine angepasste Migration; keine pauschale Baseline setzen.
Neue Version in separatem Verzeichnis bauen:
git clone --branch feature/security-rbac https://git.jfritzsche.de/jf/taskmanager.git taskmanager-next
cd taskmanager-next
git rev-parse HEAD # Release-Commit im Änderungsprotokoll festhalten
cp .env.example .env
chmod 600 .env
# .env nach README konfigurieren, ohne BOOTSTRAP-Werte und ohne DEMO_MODE.
docker compose -p taskmanager-next build
DATABASE_URL zeigt in der neuen Compose-Installation auf db:5432/taskmanager. Neue sichere DB-Zugangsdaten sind möglich, weil eine neue DB aus Dump aufgebaut wird. NEXTAUTH_URL bleibt die endgültige öffentliche URL; SMTP/OIDC/Webhooks für die Probe nicht aktivieren, Worker nicht starten. ARCHIVE_RETENTION_DAYS=0 belassen. Für eine zeitgleiche Probe App-Port über ein separates Compose-Override auf einen freien Loopback-Port ändern; die Produktionsdomain noch nicht umschalten.
2. Wartungsfenster und zusammengehöriges Backup
Proxy in Wartungsmodus setzen. Alte Anwendung und gegebenenfalls separate alte Worker stoppen; DB läuft zum Sichern weiter. Nutzeränderungen ab diesem Zeitpunkt verhindern.
set -euo pipefail
BACKUP_DIR="$(pwd)/backups/pre-upgrade-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
docker stop -t 60 taskmanager-app
# Etwaige zusätzliche alte Worker ebenfalls anhalten.
docker exec taskmanager-db pg_dump -U taskmanager -d taskmanager -Fc > "$BACKUP_DIR/database.dump"
Altes Upload-Volume sichern; den folgenden Namen durch den in Schritt 1 nachgewiesenen Namen ersetzen:
OLD_UPLOAD_VOLUME='TATSAECHLICHER_ALTER_UPLOAD_VOLUME_NAME'
docker volume inspect "$OLD_UPLOAD_VOLUME"
docker run --rm --network none \
-v "$OLD_UPLOAD_VOLUME:/source:ro" -v "$BACKUP_DIR:/backup" \
alpine:3.22 tar -czf /backup/uploads.tar.gz -C /source .
(cd "$BACKUP_DIR" && sha256sum database.dump uploads.tar.gz > SHA256SUMS)
Bei Bind-Mount -v /absoluter/alter/uploadpfad:/source:ro verwenden. Bei ausschließlich im Container gespeicherten Anhängen docker cp taskmanager-app:/app/public/uploads "$BACKUP_DIR/uploads" und dieses Verzeichnis mit seiner inneren Struktur archivieren. Im Archiv müssen unmittelbar Aufgaben-Unterverzeichnisse bzw. Dateien liegen, nicht noch ein zusätzliches uploads/-Verzeichnis.
Alte .env, Compose-/Proxy-Konfiguration und Image-ID ebenfalls geschützt sichern. Dump mit pg_restore --list prüfen und in der Probe tatsächlich wiederherstellen. Backups extern kopieren, bevor Altcontainer oder Images entfernt werden. Keine alten Volumes löschen.
Bestandsmengen für den späteren Vergleich:
docker exec taskmanager-db psql -U taskmanager -d taskmanager -c \
'SELECT (SELECT count(*) FROM "User") AS users, (SELECT count(*) FROM "Task") AS tasks, (SELECT count(*) FROM "Comment") AS comments, (SELECT count(*) FROM "File") AS files;'
3. Neue, leere Zielinstallation wiederherstellen
Projekt taskmanager-next muss separat und leer sein. Existiert es bereits aus einer Probe, eine neue Zielinstallation mit neuem Projektnamen erstellen; nicht blind --clean, down -v oder bestehende Daten überschreiben. Alle nachfolgenden -p-Angaben konsistent ersetzen.
(cd "$BACKUP_DIR" && sha256sum -c SHA256SUMS)
docker compose -p taskmanager-next up -d --wait db
docker compose -p taskmanager-next exec -T db \
pg_restore -U taskmanager -d taskmanager --no-owner --no-privileges --exit-on-error < "$BACKUP_DIR/database.dump"
docker compose -p taskmanager-next run --rm --no-deps --user 0:0 \
-v "$BACKUP_DIR:/backup:ro" migrate sh -c \
'tar -xzf /backup/uploads.tar.gz -C /app/private-uploads && chown -R 1001:1001 /app/private-uploads'
Die Eigentümerkorrektur betrifft ausschließlich das neue private Upload-Volume. Normale Dienste laufen anschließend wieder als UID 1001. Nicht auf ein unkontrolliertes Host-Verzeichnis anwenden. Das alte Volume wird hier weder eingebunden noch verändert.
Eine externe PostgreSQL-Installation erfordert entsprechend angepasste DATABASE_URL und Compose-Abhängigkeiten. Ihre DB nicht durch den hier enthaltenen lokalen db-Dienst ersetzen. Die Datenmigration selbst bleibt dieselbe.
4. Migrationshistorie prüfen und Migration ausführen
docker compose -p taskmanager-next run --rm --no-deps migrate npx prisma migrate status
Noch ausstehende Migrationen sind auf diesem Stand erwartbar. Eine mit Prisma-Migrationen betriebene Altinstallation enthält _prisma_migrations; diese Tabelle ist Teil des Dumps und muss erhalten bleiben.
Sonderfall: ursprüngliches Schema wurde früher nur mit prisma db push angelegt. Prisma meldet dann eine nicht leere DB ohne Baseline (P3005). Nur wenn nachweislich das Schema der ursprünglichen Init-Migration vorliegt und diese Änderungen bereits vorhanden sind, die Init-Migration als angewandt markieren:
docker compose -p taskmanager-next run --rm --no-deps migrate \
npx prisma migrate resolve --applied 20251111164012_init
Bei Schemaabweichungen stoppen und abgleichen. Niemals die RBAC-Migration als angewandt markieren, um einen Fehler zu übergehen. Fehlgeschlagene SQL-Migrationen anhand ihres konkreten Fehlers reparieren, bevor resolve verwendet wird.
Nun eigentliche Migration:
docker compose -p taskmanager-next run --rm migrate
Der Befehl führt nacheinander aus:
prisma migrate deploy: additive Schemaänderungen und Indizes. Keine Neuinitialisierung.scripts/migrate-access.ts: eindeutige E-Mail-Normalisierung, Bestandsmitgliedschaften, alte Rollen nach RBAC, Entwertung alter Sitzungen. Transaktional und über einen Migrationsmarker wiederholbar.scripts/migrate-uploads.ts: alle referenzierten Dateien auf Vorhandensein, Größe und zugelassenen Inhalt prüfen; alte/uploads/...-Pfade in zufällige private Speicherschlüssel umstellen und kopierte Inhalte per SHA-256 vergleichen. Originaldateien bleiben als Rückfallkopie im privaten Volume erhalten. Bereits migrierte Dateien werden beim erneuten Lauf geprüft, nicht erneut kopiert.
App hängt vom erfolgreichen Migrationsdienst ab. Kein Start bei Fehlern. Die Dateiübernahme ist pro Datei wiederaufnehmbar, nicht eine globale Dateisystemtransaktion. Nach einem Abbruch Ursache beheben und erneut ausführen. Keine parallelen Migratoren und keine schreibenden alten/neuen Apps währenddessen betreiben.
Welche Daten bleiben erhalten?
| Bestand | Verhalten |
|---|---|
| Benutzer-ID, Name, Passwort-Hash, alte Rolle | Erhalten; vorhandene Passwörter funktionieren weiter. E-Mail wird getrimmt und kleingeschrieben; kollidierende Adressen stoppen die Migration. |
| Aufgaben-ID, Beschreibung, Standort, Fälligkeit, Status, Ersteller/Zuweisung, Zeitstempel | Erhalten; alle Bestandsaufgaben gehören zunächst zur Gruppe Bestand (legacy). |
beauftragtAm, firmaBeauftragt |
Unverändert in der Datenbank/API erhalten. In der aktuellen Aufgabenmaske gibt es dafür keine eigenen Eingabefelder. |
| Kommentare, Autoren, Zuordnungen, Zeitstempel | Unverändert erhalten. |
| Dateieinträge, IDs, Namen, Zuordnungen, Originalbytes | Erhalten. Nur Speicherpfad und ermittelter MIME-Typ ändern sich. |
| Neue Felder | Sichere Standardwerte, z.B. keine Archivierung, keine Wiederholung, leere Checklisten/Tags. Ein historisches Abschlussdatum wird nicht erfunden. |
| Sitzungen | Absichtlich ungültig; erneute Anmeldung erforderlich. |
| Nicht referenzierte Altdateien | Physisch im privaten Volume erhalten, nicht automatisch einer Aufgabe zugeordnet. |
Fehlende oder ungültige Anhänge werden nicht übersprungen oder gelöscht. Anhand des Backups korrigieren bzw. fachlich prüfen. Der ursprüngliche Upload prüfte nur den behaupteten MIME-Typ; die neue Inhaltsprüfung kann deshalb alte Problemdateien erkennen. Ohne Zugriff auf reale Bestandsdaten kann keine pauschale Garantie für beschädigte oder fehlende Dateien gegeben werden.
Alte Rollen werden übernommen
| Alte Rolle | Neue direkte Rollen |
|---|---|
| ADMIN | Systemadministrator / GLOBAL |
| BEARBEITER | Bearbeiter / ASSIGNED |
| PFLEGER | Koordinator / GLOBAL und Bearbeiter / ASSIGNED |
| VORGESETZTER | Teamleitung / GLOBAL |
Alle Bestandsbenutzer erhalten Mitgliedschaft in Bestand. Alte globale Rechte anschließend organisatorisch prüfen und passende Gruppen anlegen. Teamleitung erhält nicht automatisch Rechteverwaltung. Keine Gruppen werden aus Namen oder Aufgabenorten erraten. Es erfolgt keine automatische Kontenzusammenführung und kein Seed überschreibt alte Konten.
5. Abnahme vor Umschalten
Zuerst Mengen vergleichen:
docker compose -p taskmanager-next exec -T db psql -U taskmanager -d taskmanager -c \
'SELECT (SELECT count(*) FROM "User") AS users, (SELECT count(*) FROM "Task") AS tasks, (SELECT count(*) FROM "Comment") AS comments, (SELECT count(*) FROM "File") AS files;'
docker compose -p taskmanager-next exec -T db psql -U taskmanager -d taskmanager -c \
'SELECT count(*) AS unmigrated_files FROM "File" WHERE path LIKE '\''/uploads/%'\'';'
docker compose -p taskmanager-next up -d --wait app
curl --fail http://127.0.0.1:3000/api/health
unmigrated_files muss 0 sein. Datei-Migration prüft jeden DB-Anhang; zusätzlich repräsentative alte Dateien im Browser herunterladen und mit Originalen vergleichen. Vorhandene Konten, Kommentare, Zuweisungen, erledigte Aufgaben und Beauftragungsdaten prüfen. Admin- und Bearbeiter-Login sowie fehlende Rechte, Uploads, Statuswechsel und Gruppenverwaltung testen. Neue private Dateien dürfen nicht über alte /uploads-URLs erreichbar sein. Ein Reverse Proxy darf alte Uploads auch nicht selbst statisch ausliefern.
Nach Abnahme produktive externe Dienste konfigurieren, genau einen Worker starten und erst dann Wartungsmodus aufheben:
docker compose -p taskmanager-next up -d --wait app worker
docker compose -p taskmanager-next ps -a
Der Projektname taskmanager-next bleibt danach der Name dieser Installation, auch für Backups und Updates. Beide Installationen niemals gleichzeitig öffentlich beschreibbar betreiben. Alte Installation und gesicherte Images zunächst als Rückfallbasis behalten; alte öffentliche Uploadfreigaben sperren. Originaldateien im neuen privaten Volume erst nach gesonderter Prüfung und gesichertem Restore entfernen; keine pauschale rekursive Bereinigung.
6. Rollback
Vor öffentlicher Freigabe: neue App/Worker stoppen, Proxy auf alten Stand zurückstellen, alte App mit ihrer ursprünglichen DB und ihrem ursprünglichen Upload-Volume starten. Diese wurden durch den empfohlenen Kopie-Workflow nicht verändert. Das Wartungsfenster erst nach geprüftem alten Login/Download beenden. Rückkehr zum Altstand bedeutet auch Rückkehr zu dessen bekannten Sicherheitsgrenzen.
docker compose -p taskmanager-next stop -t 60 app worker
docker start taskmanager-app
# Proxy-Routing und Wartungsmodus bewusst zurückstellen.
Nach Freigabe: zuerst neue Installation konsistent sichern. Seit dem Umstieg entstandene Aufgaben/Kommentare/Dateien wären in der alten DB nicht enthalten. Fachlich entscheiden, ob vorwärts repariert oder diese Änderungen ausdrücklich abgeglichen werden. Nicht einfach umschalten und damit Änderungen verlieren.
Bei In-place-Upgrade: alter Code allein genügt nicht. DB-Dump in eine leere Ersatzdatenbank mit pg_restore --exit-on-error --no-owner --no-privileges einspielen, dazu das zugehörige Upload-Archiv in ein leeres Ersatzvolume entpacken, alte Konfiguration/Images mit diesen Ersatzressourcen verbinden und prüfen. Kein Test-Restore in eine laufende Produktivdatenbank.
Nachweis und Grenzen
tests/upgrade.test.ts baut eine leere Testdatenbank aus dem ursprünglichen SQL-Schema auf, fügt alle vier alten Rollen sowie Aufgabe mit Beauftragungsdaten, Kommentar und verschachteltem Altanhang ein, migriert und vergleicht Bestandsfelder/Bytes. Zweiter Lauf prüft Idempotenz; fehlende private Datei wird als Fehler erkannt. Reale Produktivdaten sind damit nicht geprüft: die Restore-Probe und fachliche Abnahme der eigenen Sicherung bleiben vor dem Umstieg notwendig.