# 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.sh` → `NEW_HOST` und `scripts/prod/config.sh` → `PROD_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: ```bash 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.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 ```bash 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 ```bash # 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: ```bash # 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: ```bash 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) ```bash # 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). ```bash # 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): ```bash 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: ```bash 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 ```bash 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: ```bash # 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: ```bash 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: ```bash new_remote 'cd /opt/supabase && docker compose restart storage imgproxy' ``` ### 5.5 Daten-Verifikation ```bash # 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): > ```bash > # 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: ```yaml 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: ```conf 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: ```bash # 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**. ```bash 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. ```bash 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 # 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: ```bash 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//` nach `/opt/supabase/volumes/functions//` 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: ```bash ./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 ```bash # 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 ```bash 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 2–9 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 ```bash 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/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`): ```bash curl -s -H "apikey: " "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): ```bash 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 ```bash # 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 ```bash 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).