Why XMPP, why Prosody, why on a VPS
XMPP is the federated chat protocol that has quietly worked for twenty-five years. With OMEMO end-to-end encryption in every modern client (Conversations and Monocles on Android, Monal and Siskin on iOS, Gajim and Dino on desktop) it gives you the Signal-style properties without a phone number, and you can talk to anyone on any other server. Running your own server means your contact list, message archive and metadata live on a machine you control, under a domain you own.
Prosody is the server most self-hosters choose: small, written in Lua, a single config file, and the 13.0 branch bundles everything a mobile-friendly deployment needs. A VPS is the right home for it because federation requires a stable public address with ports 5222 and 5269 reachable from anywhere, and because push notifications and message archive only work if the server is always on. Paying for that server anonymously means the domain registration is the only paper trail, and you can register domains in crypto too.
Sizing: tiny
Prosody serves a few hundred users on a fraction of one core and under 100 MB of RAM. Disk is consumed by the message archive (MAM) and the file upload store, both of which expire on a schedule you set. The smallest plans are the correct choice.
- BVX11 (2 vCPU, 2 GB RAM, 40 GB NVMe) from $10/mo in Europe, $19 in Singapore, $32 in the US. Plenty.
- BVCX23 (2 vCPU, 4 GB, 40 GB) at $11/mo in Germany or Finland if you plan to add a TURN server for audio and video calls, which is a separate daemon (coturn) with its own memory.
- BVA11 (Arm, 2 vCPU, 4 GB, $12/mo, EU) works too; Prosody’s packages are built for arm64.
Location matters more for calls than for messages. Pick the datacenter nearest the majority of your users; messages are fine from anywhere.
DNS: A, AAAA and SRV records
Your addresses will be [email protected] while the server itself is reachable as xmpp.example.com. SRV records tell clients and other servers where to connect; without them, federation only works if example.com itself points at the server. You also want hostnames for group chats and file upload, each covered by the certificate. Create these records (TTL 3600):
xmpp.example.com. A YOUR.IPV4
xmpp.example.com. AAAA YOUR:IPV6
conference.example.com. CNAME xmpp.example.com.
upload.example.com. CNAME xmpp.example.com.
_xmpp-client._tcp.example.com. SRV 0 5 5222 xmpp.example.com.
_xmpps-client._tcp.example.com. SRV 0 5 5223 xmpp.example.com.
_xmpp-server._tcp.example.com. SRV 0 5 5269 xmpp.example.com.
_xmpp-server._tcp.conference.example.com. SRV 0 5 5269 xmpp.example.com.The _xmpps-client record advertises direct TLS on 5223 (XEP-0368), which helps clients on networks that mangle STARTTLS. The SRV for conference.example.com lets remote servers reach your group chats. If example.com has no web site you can skip the xmpp. hostname and point the SRV targets at example.com directly, but keeping web and chat on separate names is cleaner.
Prepare the server
Everything below assumes a fresh Ubuntu 24.04 image and a root shell (prefix sudo if you created a user). Patch first, then lock down SSH and the firewall before anything else is listening.
apt update && apt full-upgrade -y
apt install -y ufw unattended-upgrades curl gnupg
# SSH: keys only. Put your public key in /root/.ssh/authorized_keys first and
# confirm you can log in with it before disabling passwords.
cat > /etc/ssh/sshd_config.d/00-keys-only.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl restart ssh
# Firewall: deny everything inbound except SSH for now.
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enableInstall Prosody 13 and get certificates
Ubuntu 24.04 ships an older Prosody in its own repository; use the project’s apt repository instead. The branch package prosody-13.0 receives bug fixes nightly and will not jump to a new major version behind your back, which is what you want with unattended-upgrades enabled. Install 13.0.1 or later: 13.0.0 had a bug with direct-TLS certificate selection that is fixed in 13.0.1.
apt install -y lsb-release wget
wget -O /etc/apt/sources.list.d/prosody.sources https://prosody.im/downloads/repos/$(lsb_release -sc)/prosody.sources
apt update
apt install -y prosody-13.0 certbot
prosodyctl about | head -3Certificates: certbot in standalone mode needs port 80 briefly, and Prosody cannot read /etc/letsencrypt directly because the keys are root-only. The supported pattern is a certbot deploy hook that imports the certificates into /etc/prosody/certs with prosodyctl, which also reloads Prosody. One certificate covers all four names.
ufw allow 80/tcp comment 'certbot http-01'
certbot certonly --standalone --agree-tos --no-eff-email -m [email protected] \
-d example.com -d xmpp.example.com -d conference.example.com -d upload.example.com
cat > /etc/letsencrypt/renewal-hooks/deploy/prosody.sh <<'EOF'
#!/bin/sh
/usr/bin/prosodyctl --root cert import /etc/letsencrypt/live
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/prosody.sh
/etc/letsencrypt/renewal-hooks/deploy/prosody.sh
ls /etc/prosody/certs/Configure Prosody for OMEMO, mobile clients and federation
OMEMO itself needs nothing special on the server beyond PEP (enabled by default); the modules that make it usable across several devices and on phones are smacks (stream resumption), carbons (every device sees every message), mam (archive so a device that was offline catches up), csi_simple (battery savings), cloud_notify (push for iOS and Android) and http_file_share (XEP-0363 uploads for images and voice messages, the successor to the community mod_http_upload). All are in the 13.0 core. Replace the shipped config with this one.
cp /etc/prosody/prosody.cfg.lua /etc/prosody/prosody.cfg.lua.dist
cat > /etc/prosody/prosody.cfg.lua <<'EOF'
admins = { "[email protected]" }
pidfile = "/run/prosody/prosody.pid"
modules_enabled = {
-- required
"disco"; "roster"; "saslauth"; "tls"; "dialback";
-- multi-device and mobile
"carbons"; "smacks"; "mam"; "csi_simple"; "cloud_notify";
"pep"; "vcard4"; "vcard_legacy"; "bookmarks"; "private";
-- housekeeping
"blocklist"; "limits"; "ping"; "time"; "version"; "uptime";
"invites"; "invites_adhoc"; "invites_register";
"admin_adhoc"; "admin_shell";
"server_contact_info";
}
-- No open registration; create users with prosodyctl or send invites.
allow_registration = false
-- TLS everywhere
c2s_require_encryption = true
s2s_require_encryption = true
s2s_secure_auth = true
c2s_direct_tls_ports = { 5223 }
-- Archive: keep a month so a phone that was off for a week still syncs.
archive_expires_after = "1m"
default_archive_policy = true
max_archive_query_results = 50
-- Rate limits per connection
limits = {
c2s = { rate = "10kb/s" };
s2sin = { rate = "30kb/s" };
}
authentication = "internal_hashed"
storage = "internal"
log = {
info = "/var/log/prosody/prosody.log";
error = "/var/log/prosody/prosody.err";
}
certificates = "certs"
http_ports = { 5280 }
http_interfaces = { "127.0.0.1" }
https_ports = { 5281 }
https_interfaces = { "*", "::" }
VirtualHost "example.com"
contact_info = {
admin = { "mailto:[email protected]" };
}
Component "conference.example.com" "muc"
name = "Group chats"
restrict_room_creation = "local"
modules_enabled = { "muc_mam" }
muc_log_by_default = true
Component "upload.example.com" "http_file_share"
http_file_share_size_limit = 50 * 1024 * 1024 -- 50 MB per file
http_file_share_daily_quota = 500 * 1024 * 1024 -- per user per day
http_file_share_expires_after = 60 * 60 * 24 * 14 -- two weeks
EOF
prosodyctl check config
prosodyctl adduser [email protected]
systemctl enable --now prosody
systemctl restart prosody
ufw allow 5222/tcp comment 'xmpp c2s'
ufw allow 5223/tcp comment 'xmpp c2s direct tls'
ufw allow 5269/tcp comment 'xmpp s2s'
ufw allow 5281/tcp comment 'prosody https uploads'
prosodyctl check # DNS, certificates, config
prosodyctl check connectivity # asks an external service to connect to youprosodyctl check is the tool to trust: it verifies that SRV records resolve, that each hostname has a matching certificate, and that the ports are reachable from the outside. Log in with Conversations or Gajim as [email protected], enable OMEMO in a chat with a second account, and confirm fingerprints out of band.
Reach the server over Tor as well
A Tor onion service gives clients a way to reach the server without the VPS IP appearing on their network, and gives you a path to the server that survives a blocked IP. Clients such as Conversations (with Orbot) and Gajim can be told to connect to an explicit hostname and port rather than resolving SRV; the TLS certificate still matches example.com because that is what the client checks, so no extra certificate is needed. Federation stays on the clearnet unless both sides deploy the community mod_onions module.
apt install -y tor
cat >> /etc/tor/torrc <<'EOF'
HiddenServiceDir /var/lib/tor/xmpp/
HiddenServicePort 5222 127.0.0.1:5222
HiddenServicePort 5223 127.0.0.1:5223
HiddenServicePort 5269 127.0.0.1:5269
EOF
systemctl restart tor && sleep 5 && cat /var/lib/tor/xmpp/hostnameIn Conversations: account settings, Advanced, set the hostname to the onion address and the port to 5222, and enable Tor in the app’s connection settings. Onion services for XMPP are allowed under our acceptable use policy; running a Tor exit node on the same server is not.
Operations: updates, backups, monitoring, firewall
- Updates: unattended-upgrades picks up prosody-13.0 point releases from the project repository. Moving to 14.0 when it ships is apt install prosody-14.0, after reading the release notes.
- Certificates renew via the certbot timer; the deploy hook imports and reloads. Check with certbot renew --dry-run once; prosodyctl check certs reports expiry.
- Backups: /etc/prosody (config and certs) and /var/lib/prosody (accounts, rosters, archive, uploads). Stop Prosody or accept a slightly inconsistent copy; the internal storage is plain files. Encrypt and ship off-box nightly with restic or rsync; the message archive is sensitive even though OMEMO payloads are ciphertext.
- Monitoring: prosodyctl shell then c2s:show() and s2s:show() list live sessions; tail /var/log/prosody/prosody.err for certificate and s2s errors. mod_http_openmetrics exposes Prometheus metrics if you want graphs.
- Firewall: ufw status should list OpenSSH, 80 (certbot renewals; you can close it and use the DNS challenge), 5222, 5223, 5269 and 5281. Port 5280 is bound to localhost.
- Abuse: with registration off and invites on, you decide who gets an account. If you open registration later, add mod_register_limits and watch for spam joins to your group chats.
Which plan to pick
BVX11 (2 vCPU, 2 GB, 40 GB NVMe, from $10/mo in Germany or Finland) runs Prosody for a family or a community of a few hundred with room for Tor; choose BVCX23 ($11/mo, 4 GB) if you intend to add coturn for calls. Configure a server with Ubuntu 24.04, pay in Bitcoin or Monero for the term you want, create the DNS records while it provisions, and you can be federating within the hour.