# Production migration 0.11.9 → 13.0 (verified 2026-08-30, chat.nixg.ru / /opt/icq) Live migration of the production XMPP server from Prosody 0.11.9 to 13.0 (image `gitea.nixg.ru/hermes/icq-prosody:13.0`). Complements the parallel-stand recipe (`prosody-13-parallel-stand.md`) with the PROD-only steps and two regressions that only surface with real bridges/users. ## Steps 1. **Backup first**: `cd /opt/icq && tar czf backups/pre-migration-13-$(date +%Y%m%d_%H%M%S).tar.gz config data certs modules webchat` 2. **Swap image** in docker-compose.yml: `prosody/prosody:latest` → `gitea.nixg.ru/hermes/icq-prosody:13.0` (also remove obsolete top-level `version: "3.9"` — Compose v2 warns). 3. `docker compose up -d prosody` then `docker exec icq-prosody prosodyctl check config`. ## Config changes 0.11.9 → 13.0 (prod) 1. `component_ports = { 5347 }` — **REMOVED** in 13.0 (listener hardcoded on 5347; delete the line). The module-less `Component "telegram.nixg.ru"` block already raises the listener. 2. **`"pubsub"` out of `modules_enabled`** on the VirtualHost → must be a separate `Component "pubsub." "pubsub"`. Without this, startup fails: `Error initializing module 'pubsub' on '': Pubsub should be loaded as a component`. 3. **HTTP Upload regression — Slidge gets "No upload slot"**: - Symptom: slidge logs floods of `No upload slot in this IQ: ` (~180 / 10 min on a busy bridge). Attachments (images/videos) silently fail. - Cause: the 13.0-era community mod_http_upload only hands out slots to `origin.type == "c2s"` **or** JIDs in `http_upload_access`. A Slidge external component requests slots as a component → denied → empty result IQ. - Fix, inside the upload component block: ```lua Component "upload." "http_upload" http_upload_access = { "telegram." } ``` - After fix: 0 errors. (`docker logs icq-slidgram --since 2m | grep -c "No upload slot"` = 0.) 4. `cross_domain_websocket` is deprecated in 13.0 but STILL read by mod_websocket → keep it. (New mechanism is `http_cors_override`; no need to migrate yet.) 5. Single global `log` block (13.0 tolerates only one). ## Post-migration verification (no client password needed) - `prosodyctl check config` → All checks passed. ## WebSocket SASL regression after 13.0: "no-auth-mech" / blank spinner **Symptom:** Converse.js on https://chat.nixg.ru shows the login form for a split second, then an infinite white spinner. Browser devtools on the WS frame shows `` or the server offers NO SASL mechanisms. Prosody logs flood every ~1s with: `warn No stream features to offer on insecure session. Check encryption and security settings.` **Cause:** TLS is terminated at the edge (Caddy → nginx → Prosody over plain HTTP :5280), so the WebSocket session arrives at Prosody as *insecure*. In 13.0 mod_websocket only offers SASL on secure sessions; insecure → empty `` → client cannot authenticate and reconnects forever. (On 0.11.x this silently worked.) **Fix:** in the GLOBAL config section (before VirtualHost), set ```lua consider_websocket_secure = true ``` This is mod_websocket's own option (`module:get_option_boolean("consider_websocket_secure")`, mod_websocket.lua:35, 286 — `session.secure = consider_websocket_secure or request.secure or session.secure`). - `trusted_proxies` does NOT fix this — it only affects IP/logging, not session secure-ness. - Do NOT set `c2s_require_encryption = false` — that would allow plaintext passwords on the external 5222 port. The webchat path is already TLS all the way to the browser. **Verify:** after restart, prosody.log shows `Authenticated as user@nixg.ru [prosody:operator]` for WS logins and the `No stream features...` warnings stop. Headless check: `chromium --headless --no-sandbox --disable-gpu --virtual-time-budget=20000 --dump-dom https://chat.nixg.ru/ | grep -c converse-login-form` → 1. - Component auth: `grep "External component successfully authenticated" prosody.log` (timestamp after restart). - mod_privilege XEP-0356 live: `grep "privilege" prosody.log | grep "Archiving stanza"` — slidge-carbon-* stanza entries prove privileged message/roster delivery works on prod. - WebSocket path: raw handshake to the webchat port → `101 Switching Protocols` confirms nginx → Prosody 13.0. - SASL logic: slixmpp/raw connect with a WRONG password → `AUTH FAILED` proves the auth chain works. (Do not connect a second component with the same JID as the live bridge — Prosody logs `Second component attempted to connect, denying connection`; that is normal, not an error.) ## Gotchas - `docker exec icq-prosody sh -c 'ss/netstat...'` may show nothing if net-tools absent — rely on `prosody.log` ("Servers started", "Certificates loaded") and external handshakes instead. - Test-stand teardown for "stop until next update" (NOT delete): `docker stop icq-prosody-test && docker update --restart=no icq-prosody-test`. - Prod was re-verified healthy after: prosody/slidgram/webchat all Up, 0 upload errors.