--- name: xmpp-server-prosody title: "XMPP Server Deployment (Prosody)" description: "Deploy federated XMPP (Prosody in Docker) behind a proxy." --- # XMPP Server Deployment (Prosody) Deploy a self-hosted federated XMPP messenger (family/team) with **Prosody** in Docker, exposed through a public VPS reverse proxy. Produced from the ICQ project (chat.nixg.ru) on the bigbox/vps02 infrastructure. ## When to use - User wants a self-hosted messenger (family, team) with iOS/Android clients and **federation** (talking to the whole XMPP network). - Choosing XMPP: it is the federated standard (like email). **Do not** attempt to "clone Telegram/MTProto" — it is closed, centralised, non-federated. XMPP is the right answer when federation matters. - Snikket vs Prosody: Snikket server is easier but assumes direct 443 access and is finicky behind a reverse proxy. **Prosody** is the right choice when going through a Caddy/reverse-proxy + WireGuard setup. ## Architecture (this environment) ``` [Clients: Monal/Snikket/Conversations iOS+Android] │ c2s 5222 (TLS) ▼ [bigbox] Prosody Docker (/opt/icq) — no public IP │ s2s 5269 (federation) · http 5280 (websocket/BOSH) ▼ [vps02] Caddy (host-network, reverse_proxy 10.8.0.2:5280) + iptables DNAT 5222/5269 → 10.8.0.2 ▲ └─ WireGuard wg0 (10.8.0.x) ``` ## DNS records (critical for federation) ``` A chat.nixg.ru → public IP of vps02 SRV _xmpp-client._tcp.chat.nixg.ru → chat.nixg.ru:5222 (priority 0, weight 5) SRV _xmpp-server._tcp.chat.nixg.ru → chat.nixg.ru:5269 (priority 0, weight 5) A conference.chat.nixg.ru → public IP (MUC component host) ``` - **Without SRV records, other servers cannot find you → federation silently fails.** For a standard XMPP domain hosted at a subdomain, prosodyctl suggests using SRV to redirect; that is fine. - A-record alone works for clients hardcoded to the domain, but SRV is what the federated network uses. ## docker-compose.yml ```yaml services: prosody: image: prosody/prosody:latest container_name: icq-prosody restart: unless-stopped hostname: chat.nixg.ru volumes: - ./data:/var/lib/prosody - ./config:/etc/prosody - ./certs:/etc/prosody/certs - ./modules:/etc/prosody/modules - ./logs:/var/log/prosody ports: - "5222:5222" # c2s clients - "5269:5269" # s2s federation - "5280:5280" # BOSH/websocket (behind Caddy) # - "5281:5281" # https — SKIP unless you have a cert wired ``` Remove the obsolete `version:` attribute (Compose v2 warns). ## prosody.cfg.lua pitfall checklist - **`modules_enabled` must NOT contain `muc`** — MUC is a Component (`Component "conference.chat.nixg.ru" "muc"`). Loading `muc` as a module errors out. - **`muc_mam`** loads only on a MUC component, not on the host. - **`log` block must be in the GLOBAL section**, above any VirtualHost/Component, or `prosodyctl check config` complains. - **Community modules NOT in the stock image** (prosody/prosody:latest): `http_upload`, `smacks`, `s2s_bidi`, `xmpp_component`. They come from the prosody-modules community repo — either install them or leave them out. HTTP Upload (XEP-0363, needed for bot file delivery) requires installing `mod_http_upload` manually. - **`prosodyctl register `** works inside the container; do NOT use `-it` (fails "cannot attach stdin"). Data dir is URL-encoded: `/var/lib/prosody/chat%2enixg%2eru/accounts/`. - **https 5281 bind error** ("No certificate present") is benign — the stock image tries to bind it by default. Disable by not mapping the port; it does not break c2s/s2s/http. - **Prosody 13.0 != 0.11 for external components**: `component_ports` was REMOVED (listener is hardcoded 5347); a module-less `Component "jid"` block is required to raise the listener; `component_interfaces` must be in the GLOBAL section; `log = {...}` may only appear ONCE in the config. mod_privilege (XEP-0356) needs the TIP version (promise API) and must be enabled on BOTH the component block and the VirtualHost. Full recipe + gotchas: `references/prosody-13-parallel-stand.md`. - Validate: `docker exec icq-prosody prosodyctl check config` → "All checks passed". - `allow_registration = true` enables in-band registration — flip off after family accounts are created. ## Exposing through vps02 (Caddy + WireGuard + iptables) Caddy on vps02 runs `network_mode: host` with a plain Caddyfile. Add: ``` chat.nixg.ru { reverse_proxy 10.8.0.2:5280 { header_up Host {host} header_up X-Forwarded-Proto https } } ``` Caddy auto-issues Let's Encrypt TLS. Reload: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`. For non-HTTP ports (5222 c2s, 5269 s2s) Caddy cannot proxy them — use iptables DNAT on vps02. Existing pattern (mirrors the 8443 rule already present): ```bash sudo iptables -t nat -A PREROUTING -p tcp --dport 5222 -j DNAT --to-destination 10.8.0.2:5222 sudo iptables -t nat -A PREROUTING -p tcp --dport 5269 -j DNAT --to-destination 10.8.0.2:5269 sudo iptables -I INPUT -p tcp --dport 5222 -j ACCEPT sudo iptables -I INPUT -p tcp --dport 5269 -j ACCEPT sudo iptables -I FORWARD 1 -d 10.8.0.2 -p tcp --dport 5222 -j ACCEPT sudo iptables -I FORWARD 1 -d 10.8.0.2 -p tcp --dport 5269 -j ACCEPT sudo iptables -t nat -A POSTROUTING -o wg0 -p tcp --dport 5222 -j MASQUERADE sudo iptables -t nat -A POSTROUTING -o wg0 -p tcp --dport 5269 -j MASQUERADE # persist: sudo sh -c "iptables-save > /etc/iptables/rules.v4" ``` - **FORWARD policy is DROP** by default on vps02 — you MUST add FORWARD ACCEPT rules (the 8443 precedent has them). DNAT alone is not enough. - Save via `sudo sh -c "iptables-save > /etc/iptables/rules.v4"` — plain `sudo iptables-save > file` fails (redirect runs as your user, not root). `netfilter-persistent` service is enabled. ## Testing the external path — hairpin NAT trap - **Do NOT test a host's public ports from the same host** (or from a host that routes back into it). `ip route get ` shows `` → traffic never leaves the box, DNAT never fires. `Connection refused` from such tests is a FALSE NEGATIVE. - Validate the path instead: - From vps02 → `10.8.0.2:5222` over WG (real path, with tcpdump on bigbox wg0 shows the TCP handshake). - From an EXTERNAL host (phone on mobile data, another VPS): `nc -vz chat.nixg.ru 5222`. - If both internal paths work but an external probe fails, suspect the **provider's cloud firewall panel** (Timeweb etc.) — ports 5222/5269 are commonly closed there by default. Ask the user to open them in the provider panel. - 443 via Caddy IS externally testable from anywhere; a working 443 + working WG path to 5222 usually means only the provider firewall is in the way. ## Bots (slixmpp) and file delivery - An XMPP bot is just a second user account (JID) — no separate Bot API. Python `slixmpp` is the live, mature library. - To send files (e.g. a book-download bot), the server needs HTTP Upload (XEP-0363) — community module `mod_http_upload` (not in stock image). The client sends the file to the upload component; the recipient gets a link. - Bot pattern: user messages `book@chat.nixg.ru` → bot queries an OPDS catalog (Flibusta et al.) → downloads epub/fb2 → uploads via HTTP Upload → sends link. ## References - `references/vps02-bigbox-wg-access.md` — exact vps02/bigbox network topology, Caddyfile, iptables persistence and hairpin-test evidence.