Files
ChatApp/docs/migration-netralax-de.md
T
byGalax 5bc30c950c feat(infra): migrate self-hosted backend to netralax.de
Move Supabase + LiveKit from the netralax.cloud VPS to a new netralax.de server. Adds the migration runbook (docs/), one-time move scripts (scripts/migrate/), and prod Caddy/LiveKit config templates (infra/). Repoints the desktop publish/changelog URLs and prod ops config to .de. JWT_SECRET + VAPID copied identically so already-installed clients keep working; the new server also serves the legacy .cloud hostnames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:39:04 +02:00

45 KiB
Raw Blame History

Migrations-Runbook: Self-Hosted Backend von *.netralax.cloud auf *.netralax.de (neuer VPS)

Zweck: Vollständiger Umzug des selbstgehosteten Chat-Backends (Supabase + LiveKit/coturn + Update-Host) vom ALTEN VPS (46.225.156.249, *.netralax.cloud) auf einen FRISCHEN, leeren NEUEN VPS, der danach *.netralax.de UND während der Übergangsphase weiterhin *.netralax.cloud ausliefert.

Lesbar als: Copy-paste-Runbook. Überschriften und Erklärungen sind deutsch; alle Befehle, Pfade, Variablennamen und Konfig-Snippets bleiben wörtlich/literal.


⚠️ Zwei nicht verhandelbare Kontinuitäts-Garantien (vor allem anderen lesen)

Die bereits installierten Desktop- (Vite/electron) und Mobile- (Expo) Clients tragen die ALTEN Hostnamen und den anon-JWT fest im Bundle einkompiliert (SUPABASE_URL, LIVEKIT_URL, SUPABASE_ANON_KEY, VITE_VAPID_PUBLIC_KEY, Update-Host). Daraus folgen zwei Garantien, deren Verletzung alle bestehenden Installationen sofort und lautlos zerstört:

  1. JWT_SECRET (und damit ANON_KEY, SERVICE_ROLE_KEY) MÜSSEN byte-für-byte vom ALTEN Server übernommen werden. Der anon-JWT in den Bundles ist mit dem alten JWT_SECRET signiert. Ein anderes Secret → Kong/PostgREST/GoTrue verwerfen jedes Token → alle Sessions fallen aus, niemand kann sich mehr anmelden. Es gibt keine Fehlermeldung, die das offensichtlich macht.
  2. Das VAPID-Schlüsselpaar (VAPID_PUBLIC_KEY + VAPID_PRIVATE_KEY) MUSS identisch übernommen werden. Bestehende Web-Push-Subscriptions sind an den öffentlichen VAPID-Key gebunden. Ändert er sich, brechen alle vorhandenen Push-Abos Benachrichtigungen verstummen lautlos.

Zusätzlich: Der NEUE Caddy muss die Legacy-Vhosts *.netralax.cloud mitbedienen und die .cloud-DNS-A-Records müssen auf die NEUE IP zeigen, sonst sterben alte Clients in dem Moment, in dem der alte VPS abgeschaltet wird.


0. Voraussetzungen & Übersicht

0.1 Architektur (unverändert auf beiden Servern)

Komponente Verzeichnis Intern Öffentlich (neu) Öffentlich (Legacy, weiter bedient)
Supabase (Postgres 17, GoTrue, PostgREST, Realtime, Storage, Kong, edge-runtime, Mailpit) /opt/supabase Kong 127.0.0.1:8000 supabase.netralax.de supabase.netralax.cloud
LiveKit SFU (Signaling-WS) /opt/livekit 127.0.0.1:7880 livekit.netralax.de livekit.netralax.cloud
coturn (TURN/TURNS) /opt/livekit :3478, :5349 (TLS) turn.netralax.de:5349 turn.netralax.cloud:5349
Update-Host (electron-updater) /var/www/updates/windows file_server update.netralax.de update.netralax.cloud

TLS-Terminierung für Supabase/LiveKit/Update via Caddy (automatisches Let's Encrypt). TURNS auf 5349 läuft NICHT über Caddy und braucht ein eigenes Zertifikat auf der Platte.

0.2 Was du brauchst

  • SSH-Zugang: User prox auf dem alten (.cloud) VPS, User debian auf dem neuen (.de) VPS. Der neue VPS ist leer.
  • Die NEUE öffentliche IP des .de-VPS: 141.95.34.204 (bereits in scripts/migrate/config.shNEW_HOST und scripts/prod/config.shPROD_SERVER eingetragen). Login-User: debian.
  • Lese-Zugriff auf die ALTE /opt/supabase/.env (enthält alle zu kopierenden Secrets).
  • DNS-Verwaltung für netralax.de und netralax.cloud.
  • Entwickler-Laptop mit Bash (Linux/macOS/WSL), ssh, rsync, openssl.
  • Ein Wartungsfenster (Schreibstopp auf der App), siehe Abschnitt 5.

0.3 Reihenfolge der Arbeit (Überblick)

1. DNS vorbereiten (niedrige TTL setzen, noch NICHT umbiegen)
2. Neuen VPS bootstrappen        → scripts/migrate/01-bootstrap-new-server.sh
3. Secrets 1:1 in /opt/supabase/.env übernehmen (JWT_SECRET/VAPID identisch!)
4. Stacks LEER hochfahren (init der Rollen)
5. Wartungsfenster: DB + Storage migrieren → scripts/migrate/02-migrate-data.sh
6. LiveKit/coturn Prod-Config + Firewall + TURNS-Zertifikat
7. Caddy mit BEIDEN Domain-Sätzen (.de + .cloud)
8. Edge-Functions + deren Secrets deployen
9. Update-Host migrieren + Dual-Publish (.de UND .cloud)
10. Cutover: DNS scharf schalten (alle .de + Repoint aller .cloud)
11. Smoke-Tests (inkl. ALTER .cloud-Client)
12. Repo-Edits + neues Desktop-/Mobile-Release ausliefern
13. Rollback-Plan (bereithalten)
14. Aufräumen / .cloud später abschalten

0.4 Konventionen der Migrate-Skripte (Interface-Contract)

Alle scripts/migrate/*-Skripte sourcen scripts/migrate/config.sh. Dieses kennt beide Hosts und ist bewusst unabhängig von scripts/prod/config.sh (das bereits auf den End-Zustand .de zeigt). config.sh exportiert:

OLD_HOST="46.225.156.249"
OLD_USER="prox"
NEW_HOST="141.95.34.204"
NEW_USER="debian"   # neuer .de-Server: User debian (alt: prox)
SUPABASE_DIR="/opt/supabase"
LIVEKIT_DIR="/opt/livekit"
OLD_SSH="${OLD_USER}@${OLD_HOST}"
NEW_SSH="${NEW_USER}@${NEW_HOST}"
SSH_OPTS="-o StrictHostKeyChecking=accept-new"

…und die Helfer old_remote() / new_remote(), die per ssh ${SSH_OPTS} zum jeweiligen Host verbinden.


1. DNS-Plan

Wichtig: In diesem Schritt wird DNS noch nicht umgebogen (außer der TTL-Absenkung). Das eigentliche Scharfschalten passiert erst im Cutover (Abschnitt 10), wenn der neue VPS vollständig steht und getestet ist.

1.1 Jetzt (Vorbereitung): TTL absenken

Setze auf allen unten genannten A-Records die TTL auf 300 Sekunden (5 min), mindestens 2448 h vor dem geplanten Cutover. So wird die spätere Umstellung schnell wirksam.

1.2 Beim Cutover (Abschnitt 10): A-Records auf 141.95.34.204

Neue .de-Records (anlegen):

Record Typ Ziel
supabase.netralax.de A 141.95.34.204
livekit.netralax.de A 141.95.34.204
turn.netralax.de A 141.95.34.204
update.netralax.de A 141.95.34.204

Legacy .cloud-Records (REPOINT von alter IP 46.225.156.249 auf neue IP):

Record Typ Neues Ziel
supabase.netralax.cloud A 141.95.34.204
livekit.netralax.cloud A 141.95.34.204
turn.netralax.cloud A 141.95.34.204
update.netralax.cloud A 141.95.34.204

⚠️ Den Repoint der .cloud-Records NICHT vergessen. Alle bereits installierten Clients sprechen *.netralax.cloud an. Bleiben diese Records auf der alten IP, brechen sämtliche Installationen, sobald der alte VPS abgeschaltet wird. Der neue Caddy bedient die .cloud-Vhosts mit (Abschnitt 7), und Let's Encrypt stellt für .cloud erst dann gültige Zertifikate aus, wenn die .cloud-A-Records auf die neue IP zeigen.

1.3 Verifikation nach dem Cutover

for h in supabase livekit turn update; do
  echo "== $h.netralax.de =="; dig +short $h.netralax.de
  echo "== $h.netralax.cloud =="; dig +short $h.netralax.cloud
done

Alle acht müssen 141.95.34.204 zurückgeben.


2. Neuen Server bootstrappen

Das Skript scripts/migrate/01-bootstrap-new-server.sh wird auf den neuen VPS kopiert und dort als root ausgeführt. Es ist idempotent, erfindet keine Secrets und gibt am Ende klare NEXT-STEP-Hinweise.

2.1 Skript übertragen und ausführen

# Vom Laptop aus:
scp -o StrictHostKeyChecking=accept-new \
  scripts/migrate/01-bootstrap-new-server.sh \
  debian@141.95.34.204:/tmp/

ssh -o StrictHostKeyChecking=accept-new debian@141.95.34.204 \
  'sudo bash /tmp/01-bootstrap-new-server.sh'

2.2 Was das Bootstrap-Skript tut

  • Installiert Docker Engine + compose-plugin.
  • Installiert + aktiviert ufw und öffnet die Ports (siehe Abschnitt 6 für die vollständige Liste): 22/tcp, 80/tcp, 443/tcp, 7880/tcp, 7881/tcp, 50000:50100/udp, 3478/tcp, 3478/udp, 5349/tcp, 50200:50300/udp.
  • Klont https://github.com/supabase/supabase und kopiert supabase/docker/ nach /opt/supabase (inkl. docker-compose.yml, volumes/, .env.example). Hinweis: Wir vendoren die Supabase-Compose-Datei nicht im Repo sie wird beim Bootstrap frisch geklont.
  • Legt /opt/livekit an und schreibt Platzhalter docker-compose.yml + livekit.yaml + coturn.conf.
  • Installiert Caddy und legt eine Platzhalter-/etc/caddy/Caddyfile an.
  • Legt /var/www/updates/windows an (Artefakt-Verzeichnis; Caddy-Docroot ist das Eltern-Verzeichnis /var/www/updates, siehe §7).
  • Setzt in /opt/supabase/.env die sicherheitskritischen Secrets (JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, POSTGRES_PASSWORD, …) auf den Sentinel __COPY_FROM_OLD_SERVER__, damit ein vergessener Wert laut scheitert statt still die öffentlich bekannten Upstream-Defaults zu benutzen.
  • Druckt am Ende die NEXT-STEPS: /opt/supabase/.env befüllen (Abschnitt 3), Prod-Compose + livekit.yaml + coturn.conf einsetzen (Abschnitt 6), Caddyfile einsetzen (Abschnitt 7).

Das Bootstrap-Skript erfindet KEINE Secrets. Die sicherheitskritischen Keys stehen danach auf dem Sentinel __COPY_FROM_OLD_SERVER__ (fail-loud); Custom-Secrets wie VAPID_* / PUSH_FANOUT_SHARED_SECRET sind im Upstream-.env gar nicht vorhanden und müssen ergänzt werden. Alle echten Werte kommen in Abschnitt 3 vom alten Server.


3. Secrets 1:1 übernehmen

Alle Server-Secrets leben auf dem Server in /opt/supabase/.env. Hole zuerst die ALTE Datei:

# ALTE .env lokal sichern (nur lesend, nichts ändern):
ssh -o StrictHostKeyChecking=accept-new prox@46.225.156.249 \
  'cat /opt/supabase/.env' > old.env.backup
chmod 600 old.env.backup

3.1 Entscheidungstabelle: identisch kopieren vs. auf neuen Host umstellen

Spalte „Aktion": IDENTISCH = byte-für-byte aus old.env.backup übernehmen; NEU = auf den neuen Host/Wert setzen.

Variable Aktion Woher / Neuer Wert Begründung
POSTGRES_PASSWORD IDENTISCH old.env Dump trägt Rollen-Passwort-Hashes; muss vor Restore passen, sonst können interne Dienste sich nicht an Postgres anmelden.
JWT_SECRET 🔴 IDENTISCH old.env Signiert die eingebackenen anon/service-role-JWTs. Abweichung = alle Sessions tot.
ANON_KEY 🔴 IDENTISCH old.env Eingebackener anon-JWT der Clients.
SERVICE_ROLE_KEY IDENTISCH old.env service-role-JWT für Edge-Functions/Admin-Skripte; muss zu JWT_SECRET passen.
SECRET_KEY_BASE IDENTISCH old.env Realtime (Phoenix) + Vault: signiert Channel-Tokens/Cookies.
VAULT_ENC_KEY IDENTISCH old.env Entschlüsselt vault/pgsodium-verschlüsselte Zeilen aus dem Dump.
PG_META_CRYPTO_KEY IDENTISCH old.env postgres-meta-Crypto-Key; stabil halten.
SMTP_HOST IDENTISCH old.env Magic-Link-Mailversand erhalten (externes Relay / Mailpit).
SMTP_PORT IDENTISCH old.env s.o.
SMTP_USER IDENTISCH old.env s.o.
SMTP_PASS IDENTISCH old.env s.o.
SMTP_ADMIN_EMAIL IDENTISCH old.env Absender/SPF-Konsistenz.
SMTP_SENDER_NAME IDENTISCH old.env Anzeigename konsistent.
FUNCTIONS_VERIFY_JWT IDENTISCH old.env (false) notify-push nutzt Shared-Secret-Header statt User-JWT; bleibt false.
LIVEKIT_API_KEY IDENTISCH old.env Muss = keys:-Block in livekit.prod.yaml, sonst SFU-Reject (403).
LIVEKIT_API_SECRET IDENTISCH old.env s.o.
VAPID_PUBLIC_KEY 🔴 IDENTISCH old.env Bindet bestehende Push-Abos. Abweichung = alle Push-Subscriptions tot.
VAPID_PRIVATE_KEY 🔴 IDENTISCH old.env Muss mit unverändertem Public-Key paaren.
VAPID_SUBJECT IDENTISCH old.env Konsistenz (mailto/URL).
PUSH_FANOUT_SHARED_SECRET IDENTISCH old.env x-shared-secret-Header zwischen DB-Trigger und notify-push.
SUPABASE_SERVICE_ROLE_KEY IDENTISCH = SERVICE_ROLE_KEY Edge-Function-Alias.
SUPABASE_ANON_KEY IDENTISCH = ANON_KEY Edge-Function-Alias (mint-livekit-token RLS-Client).
SITE_URL NEU https://supabase.netralax.de GoTrue-Basis-URL für Magic-Link-Redirects.
API_EXTERNAL_URL NEU https://supabase.netralax.de Öffentliche Kong-URL, die GoTrue/Studio bewerben.
SUPABASE_PUBLIC_URL NEU https://supabase.netralax.de Studio/Kong-Asset-/Link-Generierung.
ADDITIONAL_REDIRECT_URLS NEU (Superset) siehe 3.2 GoTrue-Redirect-Allow-List inkl. Deep-Link-Schemata.
SUPABASE_URL (Edge-Function) NEU https://supabase.netralax.de (oder internes Kong) Funktionen müssen es nur erreichen.
LIVEKIT_URL (Edge-Function) NEU wss://livekit.netralax.de wss-URL für neue Builds; alte Clients nutzen .cloud (vom neuen Caddy mitbedient).
DASHBOARD_USERNAME NEU frei wählbar Studio-Basic-Auth; nicht client-kritisch.
DASHBOARD_PASSWORD NEU starkes neues Passwort s.o.
POSTGRES_HOST Default db nicht host-spezifisch.
POSTGRES_DB Default postgres s.o.
POSTGRES_PORT Default 5432 (nur an localhost gebunden) s.o.
KONG_HTTP_PORT Default 8000 muss zum Caddyfile passen.
KONG_HTTPS_PORT Default 8443 (ungenutzt) Caddy terminiert TLS.

3.2 ADDITIONAL_REDIRECT_URLS (exakt, ohne Leerzeichen)

ADDITIONAL_REDIRECT_URLS=chatapp://auth/callback,netralax://auth/callback,https://supabase.netralax.de,https://supabase.netralax.cloud

⚠️ GoTrue lehnt jeden Magic-Link-Redirect ab, der nicht exakt auf der Allow-List steht. Beide Deep-Link-Schemata (chatapp://auth/callback und netralax://auth/callback) müssen drin sein, sonst scheitert der Native-App-Login.

3.3 Werte übertragen

Bearbeite /opt/supabase/.env auf dem neuen Server und setze die IDENTISCH-Werte aus old.env.backup, die NEU-Werte aus der Tabelle:

ssh debian@141.95.34.204 'sudo nano /opt/supabase/.env'

Reihenfolge-Falle: JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, POSTGRES_PASSWORD und das VAPID-Paar müssen in der .env stehen, bevor in Abschnitt 4 der Stack hochfährt und bevor in Abschnitt 5 der Restore läuft. Setze sie jetzt vollständig.

3.4 Verifikation (Hashes vergleichen, nicht Klartext loggen)

# Stelle sicher, dass die kritischen Secrets identisch sind:
for v in JWT_SECRET ANON_KEY SERVICE_ROLE_KEY POSTGRES_PASSWORD VAPID_PUBLIC_KEY VAPID_PRIVATE_KEY; do
  old=$(ssh prox@46.225.156.249 "grep -E \"^${v}=\" /opt/supabase/.env | cut -d= -f2-" | sha256sum)
  new=$(ssh debian@141.95.34.204 "grep -E \"^${v}=\" /opt/supabase/.env | cut -d= -f2-" | sha256sum)
  [ "$old" = "$new" ] && echo "OK  $v" || echo "DIFF $v  <-- FIX BEFORE RESTORE"
done

Jede Zeile muss OK sein.


4. Stacks leer hochfahren

Bevor Daten restauriert werden, muss der frische Supabase-Stack einmal hochfahren, damit die Init-Skripte die Rollen anlegen (supabase_admin, authenticator, anon, authenticated, service_role, supabase_auth_admin, supabase_storage_admin, …), Extensions und Grants. Voraussetzung: POSTGRES_PASSWORD und JWT_SECRET sind bereits identisch gesetzt (Abschnitt 3).

# DB-Container zuerst hochfahren (legt Rollen + Extensions an):
ssh debian@141.95.34.204 \
  'cd /opt/supabase && docker compose up -d db && sleep 20'

# Health-Check:
ssh debian@141.95.34.204 \
  'cd /opt/supabase && docker compose exec -T db pg_isready -U postgres'

Den vollständigen Stack (docker compose up -d) fahren wir erst nach dem Daten-Restore hoch (Abschnitt 5, Schritt 3), damit alle Dienste gegen die wiederbefüllte DB neu verbinden.


5. Datenmigration: DB + Storage

Genutzt wird scripts/migrate/02-migrate-data.sh (läuft vom Laptop, sourct config.sh, set -euo pipefail, jeder destruktive Schritt ist abgesichert).

5.1 🔴 Wartungsfenster: Schreibstopp ZUERST

Friere Schreibvorgänge ein, bevor du dumpst und bevor du Storage rsyncst. Sonst werden DB-Zeilen und Storage-Volume inkonsistent (Objekte auf der Platte ohne Metadaten-Zeile oder umgekehrt). Setze die App in Wartungsmodus / stoppe neue Uploads/Nachrichten auf dem ALTEN System.

Pre-Flight (beide Stacks gesund):

ssh prox@46.225.156.249           'cd /opt/supabase && docker compose exec -T db pg_isready -U postgres'
ssh debian@141.95.34.204 'cd /opt/supabase && docker compose exec -T db pg_isready -U postgres'

5.2 Postgres (Major-Version 17) pg_dumpall, gestreamt ALT → NEU

Faithful Full-Cluster-Dump (Rollen inkl. Passwort-Hashes + alle DBs + auth/storage/realtime/public-Schemata), direkt vom alten in den neuen Container gestreamt:

old_remote 'cd /opt/supabase && docker compose exec -T db pg_dumpall -U postgres --clean --if-exists' \
  | new_remote 'cd /opt/supabase && docker compose exec -T db psql -U postgres -d postgres -v ON_ERROR_STOP=0'

Wichtige Hinweise zu diesem Befehl:

  • pg_dumpall (nicht pg_dump) ist nötig, weil es die ROLLEN-Definitionen samt Passwort-Hashes (md5/scram) mitnimmt. Da POSTGRES_PASSWORD auf beiden Hosts identisch ist, passen die restaurierten Rollen-Passwörter zu dem, was die Dienste benutzen.
  • --clean --if-exists macht den Dump gegen den bereits initialisierten Cluster wiederholbar (droppt/erzeugt Objekte neu).
  • ON_ERROR_STOP=0 (nicht =1): pg_dumpall versucht, bereits existierende Rollen wie supabase_admin/postgres per CREATE ROLE anzulegen → harmlose „already exists"-Fehler. Mit ON_ERROR_STOP=1 würde der erste davon einen guten Restore abbrechen. =0 schluckt aber auch echte Fehler (FK/Constraint/Ownership) und hinterlässt eine teil-restaurierte DB, die „erfolgreich" aussieht. Deshalb scannt 02-migrate-data.sh den Restore automatisch: es teet die Ausgabe in ein Log, grept nach ERROR/FATAL/PANIC abzüglich der harmlosen Muster und bricht VOR dem Storage-rsync ab, falls echte Fehler übrig bleiben (bewusster Override: FORCE_RESTORE_OK=1).
  • Erfasst in einem Rutsch alle Schemata: auth (User/Identities/Sessions), storage (Buckets + Objekt-Metadaten), realtime (Tenants/Subscriptions), public (App-Tabellen), ggf. _realtime/_analytics.

🔴 Migrationen NICHT erneut anwenden. Alle Migrationen stecken bereits im Dump. scripts/prod/push-migrations.sh nach dem Restore NICHT ausführen das riskiert Drift/Duplicate-Object-Fehler.

Alternative (nur falls Cluster-Level scheitert): Single-DB pg_dump -Fc + pg_restore --clean --if-exists --no-owner, plus separat pg_dumpall --roles-only. Der pg_dumpall-Pfad oben ist für self-hosted→self-hosted vorzuziehen.

5.3 Vollständigen Stack neu hochfahren

new_remote 'cd /opt/supabase && docker compose down && docker compose up -d'

5.4 Storage-Objekte rsync (ALT → NEU)

Die Objekt-Bytes liegen unter /opt/supabase/volumes/storage (Bind-Mount → Container /var/lib/storage); die Metadaten-Zeilen kamen bereits mit dem Dump. Schreibstopp muss noch aktiv sein. Trailing-Slashes beachten:

# Direkt ALT -> NEU (Daten fließen Server-zu-Server, wenn alt den neuen erreicht):
old_remote "sudo rsync -aHAX --numeric-ids --delete \
  -e 'ssh -o StrictHostKeyChecking=accept-new' \
  /opt/supabase/volumes/storage/ ${NEW_USER}@${NEW_HOST}:/opt/supabase/volumes/storage/"

Falls die Server sich gegenseitig nicht per SSH erreichen, zwei-stufig über den Laptop:

rsync -aHAX --numeric-ids          -e "ssh ${SSH_OPTS}" ${OLD_USER}@${OLD_HOST}:/opt/supabase/volumes/storage/ ./_storage_stage/
rsync -aHAX --numeric-ids --delete -e "ssh ${SSH_OPTS}" ./_storage_stage/ ${NEW_USER}@${NEW_HOST}:/opt/supabase/volumes/storage/
  • -aHAX erhält Hardlinks/ACLs/xattrs; --delete macht das Ziel zum exakten Spiegel (nur sicher bei eingefrorenen Schreibvorgängen).

Danach Storage-Service neu starten, damit die UID-/Ownership-Erwartung passt:

new_remote 'cd /opt/supabase && docker compose restart storage imgproxy'

5.5 Daten-Verifikation

# Tabellen-/User-Counts vergleichen (Beispiel):
new_remote 'cd /opt/supabase && docker compose exec -T db psql -U postgres -d postgres \
  -c "select count(*) as users from auth.users;" \
  -c "select count(*) as objects from storage.objects;"'

02-migrate-data.sh macht zusätzlich eine Zeilen-Paritätsprüfung OLD vs NEU über tragende Tabellen (auth.users, auth.identities, public.profiles, public.messages, public.conversation_members, storage.objects) und meldet jede Abweichung — eine reine User-/Objekt-Zählung würde Teilverluste in messages/members übersehen. Ein bekanntes Objekt sollte zudem über das neue Gateway ladbar sein (Test nach Caddy-Setup, Abschnitt 11).

Cold-Volume-Copy-Alternative: Nur falls Image-Tags byte-identisch sind, kann man statt Logical-Dump beide DBs stoppen und volumes/db/data (PGDATA) plus das db-config-Named-Volume (enthält den pgsodium-Key) rsyncen. Nur mit gestoppten DBs und identischen Postgres-Image-Tags; ansonsten den Logical-Dump oben bevorzugen.


6. LiveKit/coturn Prod-Config + Firewall-Ports + TURNS-Zertifikat

Die Prod-Config unterscheidet sich von der Dev-infra/livekit/livekit.yaml im Repo. Prod setzt rtc.use_external_ip: true und enthält KEIN node_ip: 127.0.0.1 (das ist Dev-only).

🟢 Sicherster Weg — die ALTE, funktionierende Config übernehmen. Die .example-Templates sind eine Referenz; produktiv erprobt ist aber die Config, die auf dem alten Server bereits läuft. Hol dir die echten Dateien vom alten VPS und ändere nur das Nötigste — so bleibt insbesondere erhalten, wie den Clients die TURN-Server/ICE-Credentials angekündigt werden (das macht der alte livekit.yaml-turn:/rtc:-Block bzw. die coturn-user=-Zeile; mint-livekit-token liefert nur LiveKit-URL+Token, nicht die TURN-Creds):

# vom Laptop:
scp prox@46.225.156.249:/opt/livekit/livekit.yaml  ./_livekit_old.yaml
scp prox@46.225.156.249:/opt/livekit/coturn.conf    ./_coturn_old.conf
# dann NUR anpassen: external-ip (neue IP), cert/pkey-Pfade (turn.netralax.de),
# und — falls vorhanden — eine externe IP/Domain im livekit.yaml turn-Block.
# Danach als /opt/livekit/{livekit.yaml,coturn.conf} auf den neuen Server.

Wenn die alten Dateien nicht greifbar sind, nutze die Templates unten und stelle sicher, dass die coturn-user=-Credentials zu dem passen, was deine Clients heute für TURN verwenden.

6.1 Prod-Compose + livekit.yaml + coturn.conf einsetzen

Auf dem alten Server lief LiveKit/coturn über ein Compose in /opt/livekit. Das Repo liefert dafür infra/livekit/docker-compose.prod.yml.example (die Dev-infra/livekit/docker-compose.yml ist nicht prod-tauglich: coturn läuft dort mit --no-tls, ohne 5349, ohne Zertifikat). Drei Dateien auf den Server kopieren — die on-server-Namen sind bewusst livekit.yaml / coturn.conf (genau die, die auch scripts/prod/rotate-livekit-keys.sh editiert):

Repo-Template → on-server
infra/livekit/docker-compose.prod.yml.example /opt/livekit/docker-compose.yml
infra/livekit/livekit.prod.yaml.example /opt/livekit/livekit.yaml
infra/livekit/coturn.prod.conf.example /opt/livekit/coturn.conf

keys:-Block in /opt/livekit/livekit.yaml mit den Werten aus Abschnitt 3 (LIVEKIT_API_KEY / LIVEKIT_API_SECRET) füllen:

port: 7880
log_level: info

rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 50100
  use_external_ip: true
  # KEIN node_ip: 127.0.0.1 — das ist dev-only und würde alle Remote-Clients
  # ihre Medien an den eigenen Loopback schicken lassen (Call ohne Audio/Video).

keys:
  __LIVEKIT_API_KEY__: __LIVEKIT_API_SECRET__

turn:
  enabled: false   # coturn läuft separat

🔴 node_ip: 127.0.0.1 aus der Dev-Config NICHT übernehmen. Sonst verbinden Calls zwar, haben aber keinen Ton und kein Bild, weil jeder Remote-Client Medien an seinen eigenen Loopback sendet.

🔴 LIVEKIT_API_KEY/SECRET im keys:-Block MÜSSEN exakt den Edge-Function-Werten in /opt/supabase/.env entsprechen. Sonst signiert mint-livekit-token Tokens, die der SFU mit 403 ablehnt.

6.2 coturn Prod-Config einsetzen

Template: infra/livekit/coturn.prod.conf.example/opt/livekit/coturn.conf. Die Zertifikatspfade zeigen auf /etc/letsencrypt/... — genau das Verzeichnis, das das Prod-Compose read-only in den coturn-Container einhängt:

realm=netralax.de
listening-port=3478
tls-listening-port=5349
external-ip=141.95.34.204
min-port=50200
max-port=50300
cert=/etc/letsencrypt/live/turn.netralax.de/fullchain.pem
pkey=/etc/letsencrypt/live/turn.netralax.de/privkey.pem
lt-cred-mech
user=__TURN_USER__:__TURN_PASSWORD__
fingerprint
no-multicast-peers

6.3 TURNS-Zertifikat für turn.netralax.de (NICHT über Caddy)

TURNS auf 5349 geht NICHT durch Caddy coturn braucht ein eigenes TLS-Cert+Key auf der Platte (cert/pkey-Pfade oben). Ein reines Caddy-Cert deckt das nicht ab.

Zwei Wege, das Zertifikat bereitzustellen:

A) certbot standalone (empfohlen, einfachster Pfad). Schreibt direkt nach /etc/letsencrypt/live/turn.netralax.de/ — also genau die Pfade, die coturn.conf referenziert und die das Prod-Compose in den Container einhängt. Kein Kopieren nötig:

# Port 80 muss kurz frei sein (Caddy ggf. stoppen oder DNS-01 nutzen):
sudo certbot certonly --standalone -d turn.netralax.de
# Renewal-Hook, damit coturn das erneuerte Cert lädt:
sudo certbot renew --deploy-hook 'docker compose -f /opt/livekit/docker-compose.yml restart turn'

B) Caddy-Cert wiederverwenden. Caddy hat ohnehin ein gültiges Cert für turn.netralax.de, sobald der DNS-Record steht und der Host in der Caddy-Config ist. PEM/Key aus Caddys Storage (/var/lib/caddy/.local/share/caddy/certificates/...) an die /etc/letsencrypt/live/turn.netralax.de/-Pfade symlinken/kopieren und coturn nach Renewals neu starten. Umständlicher als (A) — nur, wenn certbot nicht in Frage kommt.

coturn liest das Cert beim Start; nach jeder Erneuerung den turn-Container neu starten (Hook oben). Das external-ip muss die neue öffentliche IP sein.

6.4 Firewall-Ports (ufw) ALLE öffnen, sonst kein A/V

Diese Ports umgehen Caddy und müssen direkt in ufw offen sein. Fehlt einer, haben Calls keinen Ton/kein Bild.

ssh debian@141.95.34.204 'sudo bash -s' <<'EOF'
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 7880/tcp                # LiveKit Signaling (hinter Caddy)
ufw allow 7881/tcp                # RTC TCP-Fallback
ufw allow 50000:50100/udp         # RTC Media
ufw allow 3478/udp                # coturn STUN/TURN
ufw allow 3478/tcp                # coturn STUN/TURN
ufw allow 5349/tcp                # coturn TURNS (TLS)
ufw allow 50200:50300/udp         # coturn TURN-Relay
ufw --force enable
ufw status verbose
EOF

Postgres NICHT öffentlich öffnen. 5432 bleibt nur an localhost gebunden (wie auf dem alten Server). Für Remote-psql das bestehende Tunnel-Muster nutzen: ./scripts/prod/tunnel-db.sh (SSH-Tunnel localhost:5433 → server:5432).

6.5 LiveKit-Stack starten

Voraussetzung: /opt/livekit/docker-compose.yml ist das Prod-Compose aus §6.1 (host-networking, mountet livekit.yaml + coturn.conf + /etc/letsencrypt), nicht das Dev-Compose.

ssh debian@141.95.34.204 'cd /opt/livekit && docker compose up -d && docker compose ps'
# coturn lauscht jetzt auf 5349/TLS? prüfen:
ssh debian@141.95.34.204 'ss -tlnp | grep -E "5349|3478" ; docker compose -f /opt/livekit/docker-compose.yml logs turn --tail=20'

7. Caddy mit BEIDEN Domain-Sätzen (.de + .cloud Legacy)

Template: infra/caddy/Caddyfile → auf dem Server /etc/caddy/Caddyfile. Caddy terminiert TLS (automatisches Let's Encrypt) und reverse-proxyt Klartext-HTTP an die lokalen Backends. Pro Vhost genau EIN reverse_proxy Kong multiplext bereits alle Supabase-Routen; keine Pfad-Splits in Caddy.

# Caddyfile — Dual-Domain-Übergang .cloud -> .de
#
# Während der Migration bedient dieser Caddy BEIDE Domain-Sätze aus denselben
# lokalen Backends:
#   - *.netralax.de    = neue, primäre Hostnamen (neue Client-Builds)
#   - *.netralax.cloud = Legacy-Hostnamen, die in bereits installierten
#                        Desktop-/Mobile-Bundles fest einkompiliert sind.
# Die .cloud-DNS-A-Records zeigen (nach dem Cutover) auf DIESELBE neue IP, damit
# alte Installationen weiterlaufen, bis sie sich selbst auf .de aktualisieren.
# NICHT entfernen, solange noch alte Clients .cloud ansprechen (siehe Abschnitt 14).

# AKTIV ab jetzt: nur die .de-Hosts. Die .cloud-Blöcke stehen auskommentiert
# darunter und werden ERST beim Cutover (§10) aktiviert — sonst läuft Caddy ins
# Let's-Encrypt-Rate-Limit, weil .cloud-DNS noch auf den alten Server zeigt.

# --- Supabase (Kong-Gateway :8000 multiplext auth/rest/realtime/storage/functions/Studio) ---
# Realtime-WS (/realtime/v1/websocket) wird von reverse_proxy transparent upgegradet.
supabase.netralax.de {
	reverse_proxy localhost:8000
}

# --- LiveKit Signaling-WS (:7880). Caddy reicht Upgrade/Connection-Header durch. ---
livekit.netralax.de {
	reverse_proxy localhost:7880
}

# --- Update-Host (electron-updater: latest.yml + .exe + changelog.json) ---
# 🔴 docroot ist /var/www/updates, NICHT .../windows: release.mjs lädt nach
#    /var/www/updates/windows/ hoch, Clients holen unter URL-Pfad /windows/…
#    Mit root=.../windows entstünde /windows/windows/ → 404 für JEDES Update.
update.netralax.de {
	root * /var/www/updates
	file_server
}

# --- CUTOVER (§10): erst NACH .cloud-DNS-Repoint einkommentieren + caddy reload ---
# supabase.netralax.cloud { reverse_proxy localhost:8000 }
# livekit.netralax.cloud  { reverse_proxy localhost:7880 }
# update.netralax.cloud   { root * /var/www/updates
#                           file_server }

🔴 Pfad-Matcher, die WS-Endpunkte ausschließen, sind tabu. Caddy v2 reicht WebSocket-Upgrades transparent durch aber nur, wenn der ganze Host reverse-proxyt wird (kein Sub-Path-Matching). Das gilt für Realtime (/realtime/v1/websocket) und LiveKit (/rtc). Es gibt kein „websocket"-Flag und es wird keins gebraucht.

Aktivieren:

ssh debian@141.95.34.204 'sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy'

Let's Encrypt stellt für die .cloud-Namen erst gültige Zertifikate aus, nachdem die .cloud-A-Records auf die neue IP zeigen (Cutover, Abschnitt 10). Bis dahin schlägt die Cert-Ausstellung für .cloud fehl das ist erwartbar und löst sich mit dem DNS-Repoint.


8. Edge-Functions deployen + Secrets

Edge-Functions liegen im Repo unter supabase/functions/: mint-livekit-token, notify-push, og-preview. Deploy via bestehendem Skript (kopiert supabase/functions/<name>/ nach /opt/supabase/volumes/functions/<name>/ und startet functions-Container neu).

Achtung Host-Pinning des Deploy-Skripts: scripts/prod/push-edge-function.sh sourct scripts/prod/config.sh, das auf PROD_SERVER="141.95.34.204" (neuer .de-VPS, User debian) zeigt. Diese Befehle pushen also auf den NEUEN Server — erst ausführen, nachdem Bootstrap + Secrets dort stehen:

./scripts/prod/push-edge-function.sh mint-livekit-token
./scripts/prod/push-edge-function.sh notify-push
./scripts/prod/push-edge-function.sh og-preview

8.1 Erwartete Edge-Function-Secrets in /opt/supabase/.env

Aus dem Code verifiziert; alle in /opt/supabase/.env (in Abschnitt 3 bereits gesetzt):

LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, PUSH_FANOUT_SHARED_SECRET, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_ANON_KEY.

Erinnerung: FUNCTIONS_VERIFY_JWT=false lassen (notify-push gatet über x-shared-secret-Header, nicht über User-JWT).

🔴 Custom-Secrets müssen den functions-Container auch erreichen. Im frisch geklonten Supabase-Compose bekommt der functions-Service nur die env-Variablen, die in seinem environment:/env_file:-Block stehen. LIVEKIT_API_KEY/SECRET, VAPID_*, PUSH_FANOUT_SHARED_SECRET und SUPABASE_ANON_KEY sind Custom-Variablen und stehen dort per Default nicht drin. Auf dem alten Server ist das verdrahtet (es läuft ja) — auf dem neuen muss es nachgezogen werden: entweder env_file: .env am functions-Service ergänzen oder die Variablen explizit in dessen environment: listen. Sonst sieht mint-livekit-token leere Strings → livekit-not-configured (500) und notify-push lehnt mangels SHARED_SECRET jede Anfrage ab.

8.2 Verifikation

# 1) Erreichen die Secrets den Container wirklich? (vor dem Funktionstest!)
ssh debian@141.95.34.204 'cd /opt/supabase && docker compose exec -T functions \
  env | grep -E "LIVEKIT_API_KEY|LIVEKIT_API_SECRET|VAPID_PUBLIC_KEY|PUSH_FANOUT_SHARED_SECRET|SUPABASE_ANON_KEY"'
# -> Es müssen NICHT-leere Werte erscheinen. Fehlt einer: env_file/environment im
#    functions-Service nachziehen und 'docker compose up -d functions'.

# 2) Logs:
./scripts/prod/logs.sh   # bzw. docker compose logs functions --tail=20
# 403 bei mint-livekit-token? -> LIVEKIT_API_KEY/SECRET stimmen nicht mit /opt/livekit/livekit.yaml überein.

9. Update-Host migrieren + Dual-Publish (.de UND .cloud)

Der Update-Host ist ein statisches Verzeichnis /var/www/updates/windows mit latest.yml, .exe-Installern und changelog.json, ausgeliefert per file_server (Abschnitt 7). SSH-Deploy-User: chatapp-deploy.

9.1 Bestehende Artefakte ALT → NEU spiegeln

rsync -aHAX --numeric-ids -e "ssh ${SSH_OPTS}" \
  chatapp-deploy@46.225.156.249:/var/www/updates/windows/ ./_updates_stage/
rsync -aHAX --numeric-ids -e "ssh ${SSH_OPTS}" \
  ./_updates_stage/ chatapp-deploy@141.95.34.204:/var/www/updates/windows/

9.2 Dual-Publish-Garantie

Beide Hosts (update.netralax.de und ab Cutover update.netralax.cloud) haben im Caddyfile denselben docroot /var/www/updates (nicht …/windows). Die Artefakte liegen physisch in /var/www/updates/windows/ und werden so unter dem URL-Pfad /windows/latest.yml usw. ausgeliefert unter beiden Hosts aus einem Verzeichnis. (Den Docroot-Fallstrick /windows/windows/ → 404 siehe §7.)

🔴 Alte Clients prüfen update.netralax.cloud. Liegt die Switch-over-Release nicht (auch) unter .cloud, können alte Installationen sich niemals auf .de aktualisieren. Der changelog.ts der neuen Builds zeigt zwar auf https://update.netralax.de/windows/changelog.json, aber die im Bundle der alten Clients eingebackene URL ist .cloud beide müssen funktionieren.

9.3 Deploy-Konfiguration

.env.release ist bereits gesetzt (UPDATE_HOST=update.netralax.de, UPDATE_SSH_USER=chatapp-deploy, UPDATE_REMOTE_PATH=/var/www/updates/windows). Stelle sicher, dass der Deploy-User chatapp-deploy auf dem neuen VPS existiert und Schreibrechte auf /var/www/updates/windows hat.


10. Cutover & DNS scharf schalten

Erst hier wird DNS umgebogen. Voraussetzung: Abschnitte 29 abgeschlossen, neuer VPS steht, Stacks laufen, Caddy lädt (für .de bereits mit gültigem Cert), Storage + DB migriert, Wartungsfenster ggf. noch aktiv.

10.1 Reihenfolge

  1. .de-A-Records anlegen (Abschnitt 1.2, neue Records) → Caddy holt sofort Let's-Encrypt-Certs für .de.
  2. Interner Smoke-Test über .de (Abschnitt 11) bevor alte Clients umgeschwenkt werden.
  3. .cloud-A-Records repointen auf 141.95.34.204 (Abschnitt 1.2, Legacy-Records) → Caddy stellt jetzt auch für .cloud Certs aus; alte Clients landen ab jetzt auf dem neuen VPS.
  4. Propagation prüfen (Abschnitt 1.3).
  5. Wartungsmodus aufheben, Schreibvorgänge auf dem neuen System freigeben.

10.2 Verifikation der TLS-Ausstellung

for h in supabase.netralax.de supabase.netralax.cloud livekit.netralax.de livekit.netralax.cloud update.netralax.de update.netralax.cloud; do
  echo "== $h =="
  echo | openssl s_client -connect "$h:443" -servername "$h" 2>/dev/null | openssl x509 -noout -subject -dates
done

Jeder Host muss ein gültiges, nicht abgelaufenes Cert liefern.


11. Smoke-Test-Checkliste

Nach dem Cutover, in dieser Reihenfolge:

  • https://supabase.netralax.de/auth/v1/health und https://supabase.netralax.cloud/auth/v1/health liefern 200.
  • Login per Magic-Link, pro Plattform mit dem JEWEILS registrierten Schema testen: Desktop über chatapp://auth/callback, Mobile über netralax://auth/callback (das in apps/mobile/app.json registrierte Schema). ADDITIONAL_REDIRECT_URLS enthält beide, daher akzeptiert GoTrue beides — aber das OS routet nur das tatsächlich registrierte Schema zurück in die App.

    ⚠️ Vorbestehend (nicht durch den Umzug verursacht): apps/mobile/.env.local setzt aktuell EXPO_PUBLIC_AUTH_REDIRECT_URL=chatapp://auth/callback, app.json registriert aber nur netralax://. Für funktionierende Mobile-Magic-Links sollte das App-Team den Mobile-Wert auf netralax://auth/callback setzen (Desktop bleibt chatapp://). Außerhalb des Server-Umzugs — hier nur als Flag.

  • PostgREST-Zugriff mit dem eingebackenen anon-Key wird akzeptiert (kein 401 wegen falschem JWT_SECRET):
    curl -s -H "apikey: <ANON_KEY>" "https://supabase.netralax.de/rest/v1/" | head
    

11.2 Nachricht senden / Realtime

  • Zwei eingeloggte Clients: Nachricht von A erscheint bei B in Echtzeit (Realtime-WS /realtime/v1/websocket über Caddy).
  • Storage: Upload + Re-Download eines Bildes (/storage/v1/object/...) funktioniert (DB-Metadaten + Volume-Bytes konsistent).

11.3 Voice-Call mit echtem Ton (über TURN)

  • Call zwischen zwei Geräten in unterschiedlichen Netzen (mind. eins hinter NAT/CGNAT, das TURN erzwingt): Verbindung steht und es ist echter Ton/Bild hörbar/sichtbar.
  • Bestätigt indirekt: rtc.use_external_ip: true, kein node_ip: 127.0.0.1, alle Media-Ports offen, TURNS-Cert für turn.netralax.de gültig.
  • mint-livekit-token liefert ein Token, das der SFU akzeptiert (kein 403 → Keys stimmen mit /opt/livekit/livekit.yaml überein).

11.4 Web-Push

  • Ein bestehender (vor der Migration angelegter) Push-Abonnent erhält weiterhin Benachrichtigungen → bestätigt identisches VAPID-Paar.
  • Neue Subscription + Test-Push über notify-push (mit korrektem x-shared-secret / PUSH_FANOUT_SHARED_SECRET) kommt an.

11.5 Auto-Update-Check von einem ALTEN .cloud-Client

  • latest.yml ist unter beiden Hosts mit echtem 200 abrufbar (nicht nur „erreichbar" — der Docroot-Bug aus §7 würde hier 404 liefern):
    curl -sI https://update.netralax.de/windows/latest.yml    | head -1   # HTTP/2 200
    curl -sI https://update.netralax.cloud/windows/latest.yml | head -1   # HTTP/2 200
    curl -sI https://update.netralax.cloud/windows/changelog.json | head -1
    
  • Eine bestehende, alte Desktop-Installation (Hostnamen .cloud eingebacken) prüft auf Updates: electron-updater findet die Switch-over-Release, lädt sie und installiert.
  • Nach dem Update zeigt der Client auf .de (neue Bundle-Werte) und funktioniert vollständig (Login, Nachricht, Call, Push).

Dieser letzte Test ist der wichtigste. Er beweist den gesamten Übergangspfad: alter Client → .cloud (neue IP) → lädt Update → wird zu .de-Client.


12. Repo-Änderungen + neues Release bauen/ausliefern

12.1 Bereits gemachte Edits (verifiziert im Repo)

Datei Änderung Status
scripts/prod/config.sh PROD_SERVER="141.95.34.204", PROD_DOMAIN_SUPABASE=supabase.netralax.de, PROD_DOMAIN_LIVEKIT=livekit.netralax.de erledigt (End-Zustand)
apps/desktop/.env SUPABASE_URL + VITE_SUPABASE_URL = https://supabase.netralax.de; VITE_LIVEKIT_URL=wss://livekit.netralax.de; anon-Key + VITE_VAPID_PUBLIC_KEY (unverändert übernommen) erledigt
apps/mobile/.env.local EXPO_PUBLIC_SUPABASE_URL=https://supabase.netralax.de (anon-Key, redirect-Schema unverändert) erledigt
.env.release UPDATE_HOST=update.netralax.de, UPDATE_SSH_USER=chatapp-deploy, UPDATE_REMOTE_PATH=/var/www/updates/windows erledigt
package.json release-Script + prod:*-Scripts vorhanden (unverändert; nutzen scripts/prod/config.sh) vorhanden
apps/desktop/src/lib/changelog.ts CHANGELOG_URL='https://update.netralax.de/windows/changelog.json' (mit Kommentar, dass alte Clients weiter .cloud abfragen) erledigt

Erledigt: Die neue IP 141.95.34.204 ist in scripts/prod/config.sh (PROD_SERVER) und scripts/migrate/config.sh (NEW_HOST) eingetragen; Login-User dort ist debian.

12.2 Neues Desktop-Release bauen + dual publizieren

# Vom Laptop, mit korrektem .env.release:
pnpm install
pnpm --filter @chat-app/desktop build
pnpm release        # = node scripts/release.mjs

scripts/release.mjs lädt latest.yml + .exe + aktualisiertes changelog.json nach UPDATE_HOST (update.netralax.de). Da Caddy update.netralax.de und update.netralax.cloud aus demselben Verzeichnis bedient, ist diese eine Veröffentlichung automatisch unter beiden Hosts verfügbar (Dual-Publish, Abschnitt 9).

🔴 Diese Release MUSS unter .cloud erreichbar sein, denn nur sie schaltet alte Installationen auf .de um. Nach dem Upload mit Abschnitt 11.5 verifizieren.

12.3 Neues Mobile-Release

pnpm --filter @chat-app/mobile typecheck
# Expo-Build/Submit nach eurem üblichen EAS-/Store-Prozess.
# .env.local trägt bereits EXPO_PUBLIC_SUPABASE_URL=https://supabase.netralax.de.

Mobile-Clients aktualisieren über die App-Stores, nicht über den Update-Host. Bis ein User die neue Store-Version installiert, hält ihn der .cloud-Vhost am Leben.


13. Rollback-Plan

Der alte VPS bleibt vollständig intakt und laufend, bis der neue verifiziert ist. Rollback heißt im Kern: DNS zurückbiegen.

  1. Schnell-Rollback (DNS): Alle .cloud-A-Records zurück auf 46.225.156.249 (alte IP), .de-Records entfernen oder ebenfalls auf alt zeigen lassen. Dank niedriger TTL (Abschnitt 1.1) greift das in Minuten. Alte Clients landen wieder auf dem alten, intakten Server.
  2. Voraussetzung dafür: Während der Migration keine destruktiven Änderungen am alten Server (alter Stack nicht löschen, alte Volumes nicht anfassen). Der Schreibstopp (Abschnitt 5.1) bedeutet nur Wartungsmodus, kein Datenverlust.
  3. Daten-Divergenz beachten: Wurden nach dem Cutover bereits Schreibvorgänge auf dem neuen Server akzeptiert, gehen diese bei einem reinen DNS-Rollback verloren. Deshalb: Cutover (Abschnitt 10.5, Schreibfreigabe) erst nach den Smoke-Tests; bis dahin ist der Rollback verlustfrei.
  4. Update-Host-Rollback: .exe/latest.yml auf dem alten Host wurden nicht verändert; alte Clients, die noch nicht aktualisiert haben, finden dort weiterhin den alten Stand.
  5. Wenn nur eine Komponente klemmt (z. B. nur TURN ohne Ton), kann punktuell zurückgerollt werden, indem nur der betroffene .cloud-Record zurückzeigt die übrigen können auf neu bleiben.

14. Aufräumen / .cloud später abschalten

Die .cloud-Hosts dürfen erst verschwinden, wenn praktisch keine alten Clients mehr darauf zugreifen.

14.1 Reihenfolge der Abschaltung (frühestens → spätestens)

  1. Alten VPS dekommissionieren: Erst nachdem .cloud-DNS auf den neuen VPS repointet ist und über die neue IP läuft. (Der alte Server liefert dann ohnehin keinen Traffic mehr.) Vorher als Rollback-Sicherheit behalten (Abschnitt 13).
  2. Supabase-/LiveKit-.cloud-Vhosts in Caddy entfernen, sobald Telemetrie/Logs zeigen, dass praktisch alle aktiven Sessions auf .de laufen (d. h. die meisten Desktop-Clients haben die Switch-over-Release gezogen und Mobile-Clients die neue Store-Version).
  3. Update-.cloud-Vhost als LETZTES abschalten.

14.2 Warum der Update-Host am längsten bleiben muss

Eine Desktop-Installation, die noch nie die Switch-over-Release gezogen hat, kennt nur update.netralax.cloud (eingebacken). Sie erreicht .de ausschließlich, indem sie die neue Version über .cloud herunterlädt. Schaltest du update.netralax.cloud zu früh ab, stranden alle noch nicht aktualisierten Clients dauerhaft auf der alten Version sie können sich nie mehr selbst auf .de updaten und müssten manuell neu installiert werden.

Faustregel: update.netralax.cloud so lange behalten, bis die Update-Metriken zeigen, dass der Long-Tail alter Installationen vernachlässigbar ist (eher Monate als Wochen). Supabase-/LiveKit-.cloud können früher fallen als Update-.cloud, aber niemals umgekehrt.

14.3 Endzustand

  • DNS: nur noch *.netralax.de aktiv; *.netralax.cloud entfernt (zuletzt update.netralax.cloud).
  • Caddyfile: nur noch die .de-Vhosts (Legacy-Block + Kommentar entfernt).
  • scripts/prod/config.sh ist die alleinige Live-Konfiguration; scripts/migrate/ wird nicht mehr gebraucht (kann archiviert bleiben).
  • Lokale Sicherungen (old.env.backup, _storage_stage/, _updates_stage/) sicher löschen (shred/Secure-Delete), da sie Secrets enthalten.

Grounding-Hinweise (Repo-Fakten): Postgres-Major-Version aus supabase/config.toml = 17. Edge-Functions im Repo: supabase/functions/{mint-livekit-token,notify-push,og-preview}. Dev-infra/livekit/livekit.yaml enthält absichtlich use_external_ip: false + node_ip: 127.0.0.1 (Dev-only — in Prod invertiert/entfernt). apps/desktop/.env, apps/mobile/.env.local, .env.release, scripts/prod/config.sh und apps/desktop/src/lib/changelog.ts sind bereits auf .de umgestellt (verifiziert).