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>
45 KiB
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.deUND – während der Übergangsphase – weiterhin*.netralax.cloudausliefert.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:
JWT_SECRET(und damitANON_KEY,SERVICE_ROLE_KEY) MÜSSEN byte-für-byte vom ALTEN Server übernommen werden. Der anon-JWT in den Bundles ist mit dem altenJWT_SECRETsigniert. 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.- 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
proxauf dem alten (.cloud) VPS, Userdebianauf dem neuen (.de) VPS. Der neue VPS ist leer. - Die NEUE öffentliche IP des
.de-VPS:141.95.34.204(bereits inscripts/migrate/config.sh→NEW_HOSTundscripts/prod/config.sh→PROD_SERVEReingetragen). Login-User:debian. - Lese-Zugriff auf die ALTE
/opt/supabase/.env(enthält alle zu kopierenden Secrets). - DNS-Verwaltung für
netralax.deundnetralax.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 24–48 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.cloudan. 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.clouderst 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/supabaseund kopiertsupabase/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/livekitan und schreibt Platzhalterdocker-compose.yml+livekit.yaml+coturn.conf. - Installiert Caddy und legt eine Platzhalter-
/etc/caddy/Caddyfilean. - Legt
/var/www/updates/windowsan (Artefakt-Verzeichnis; Caddy-Docroot ist das Eltern-Verzeichnis/var/www/updates, siehe §7). - Setzt in
/opt/supabase/.envdie 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/.envbefüllen (Abschnitt 3), Prod-Compose +livekit.yaml+coturn.confeinsetzen (Abschnitt 6),Caddyfileeinsetzen (Abschnitt 7).
Das Bootstrap-Skript erfindet KEINE Secrets. Die sicherheitskritischen Keys stehen danach auf dem Sentinel
__COPY_FROM_OLD_SERVER__(fail-loud); Custom-Secrets wieVAPID_*/PUSH_FANOUT_SHARED_SECRETsind im Upstream-.envgar 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/callbackundnetralax://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_PASSWORDund das VAPID-Paar müssen in der.envstehen, 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(nichtpg_dump) ist nötig, weil es die ROLLEN-Definitionen samt Passwort-Hashes (md5/scram) mitnimmt. DaPOSTGRES_PASSWORDauf beiden Hosts identisch ist, passen die restaurierten Rollen-Passwörter zu dem, was die Dienste benutzen.--clean --if-existsmacht den Dump gegen den bereits initialisierten Cluster wiederholbar (droppt/erzeugt Objekte neu).ON_ERROR_STOP=0(nicht=1):pg_dumpallversucht, bereits existierende Rollen wiesupabase_admin/postgresperCREATE ROLEanzulegen → harmlose „already exists"-Fehler. MitON_ERROR_STOP=1würde der erste davon einen guten Restore abbrechen.=0schluckt aber auch echte Fehler (FK/Constraint/Ownership) und hinterlässt eine teil-restaurierte DB, die „erfolgreich" aussieht. Deshalb scannt02-migrate-data.shden Restore automatisch: es teet die Ausgabe in ein Log, grept nachERROR/FATAL/PANICabzü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.shnach 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/
-aHAXerhält Hardlinks/ACLs/xattrs;--deletemacht 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 dasdb-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.yamlim Repo. Prod setztrtc.use_external_ip: trueund enthält KEINnode_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 altelivekit.yaml-turn:/rtc:-Block bzw. die coturn-user=-Zeile;mint-livekit-tokenliefert 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.1aus 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/SECRETimkeys:-Block MÜSSEN exakt den Edge-Function-Werten in/opt/supabase/.enventsprechen. Sonst signiertmint-livekit-tokenTokens, 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
5349geht 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). Dasexternal-ipmuss 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.
5432bleibt nur anlocalhostgebunden (wie auf dem alten Server). Für Remote-psqldas bestehende Tunnel-Muster nutzen:./scripts/prod/tunnel-db.sh(SSH-Tunnellocalhost: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.cloudfehl – 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.shsourctscripts/prod/config.sh, das aufPROD_SERVER="141.95.34.204"(neuer.de-VPS, Userdebian) 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 derfunctions-Service nur die env-Variablen, die in seinemenvironment:/env_file:-Block stehen.LIVEKIT_API_KEY/SECRET,VAPID_*,PUSH_FANOUT_SHARED_SECRETundSUPABASE_ANON_KEYsind 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: entwederenv_file: .envamfunctions-Service ergänzen oder die Variablen explizit in dessenenvironment:listen. Sonst siehtmint-livekit-tokenleere Strings →livekit-not-configured(500) undnotify-pushlehnt mangelsSHARED_SECRETjede 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.deaktualisieren. Derchangelog.tsder neuen Builds zeigt zwar aufhttps://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 2–9 abgeschlossen, neuer VPS steht, Stacks laufen, Caddy lädt (für
.debereits mit gültigem Cert), Storage + DB migriert, Wartungsfenster ggf. noch aktiv.
10.1 Reihenfolge
.de-A-Records anlegen (Abschnitt 1.2, neue Records) → Caddy holt sofort Let's-Encrypt-Certs für.de.- Interner Smoke-Test über
.de(Abschnitt 11) bevor alte Clients umgeschwenkt werden. .cloud-A-Records repointen auf141.95.34.204(Abschnitt 1.2, Legacy-Records) → Caddy stellt jetzt auch für.cloudCerts aus; alte Clients landen ab jetzt auf dem neuen VPS.- Propagation prüfen (Abschnitt 1.3).
- 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:
11.1 Supabase / Auth / Magic-Link
https://supabase.netralax.de/auth/v1/healthundhttps://supabase.netralax.cloud/auth/v1/healthliefern200.- Login per Magic-Link, pro Plattform mit dem JEWEILS registrierten Schema testen: Desktop über
chatapp://auth/callback, Mobile übernetralax://auth/callback(das inapps/mobile/app.jsonregistrierte Schema).ADDITIONAL_REDIRECT_URLSenthä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.localsetzt aktuellEXPO_PUBLIC_AUTH_REDIRECT_URL=chatapp://auth/callback,app.jsonregistriert aber nurnetralax://. Für funktionierende Mobile-Magic-Links sollte das App-Team den Mobile-Wert aufnetralax://auth/callbacksetzen (Desktop bleibtchatapp://). 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, keinnode_ip: 127.0.0.1, alle Media-Ports offen, TURNS-Cert fürturn.netralax.degültig. mint-livekit-tokenliefert 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 korrektemx-shared-secret/PUSH_FANOUT_SHARED_SECRET) kommt an.
11.5 Auto-Update-Check von einem ALTEN .cloud-Client
latest.ymlist unter beiden Hosts mit echtem200abrufbar (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
.cloudeingebacken) 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.204ist inscripts/prod/config.sh(PROD_SERVER) undscripts/migrate/config.sh(NEW_HOST) eingetragen; Login-User dort istdebian.
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
.clouderreichbar sein, denn nur sie schaltet alte Installationen auf.deum. 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.
- Schnell-Rollback (DNS): Alle
.cloud-A-Records zurück auf46.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. - 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.
- 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.
- Update-Host-Rollback:
.exe/latest.ymlauf dem alten Host wurden nicht verändert; alte Clients, die noch nicht aktualisiert haben, finden dort weiterhin den alten Stand. - 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)
- 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). - Supabase-/LiveKit-
.cloud-Vhosts in Caddy entfernen, sobald Telemetrie/Logs zeigen, dass praktisch alle aktiven Sessions auf.delaufen (d. h. die meisten Desktop-Clients haben die Switch-over-Release gezogen und Mobile-Clients die neue Store-Version). - 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.deausschließlich, indem sie die neue Version über.cloudherunterlädt. Schaltest duupdate.netralax.cloudzu früh ab, stranden alle noch nicht aktualisierten Clients dauerhaft auf der alten Version – sie können sich nie mehr selbst auf.deupdaten und müssten manuell neu installiert werden.Faustregel:
update.netralax.cloudso lange behalten, bis die Update-Metriken zeigen, dass der Long-Tail alter Installationen vernachlässigbar ist (eher Monate als Wochen). Supabase-/LiveKit-.cloudkönnen früher fallen als Update-.cloud, aber niemals umgekehrt.
14.3 Endzustand
- DNS: nur noch
*.netralax.deaktiv;*.netralax.cloudentfernt (zuletztupdate.netralax.cloud). - Caddyfile: nur noch die
.de-Vhosts (Legacy-Block + Kommentar entfernt). scripts/prod/config.shist 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).