运行我自己的XMPP服务器
Running My Own XMPP Server

原始链接: https://blog.dmcc.io/journal/xmpp-turn-stun-coturn-prosody/

## 自托管 XMPP 用于联合消息传递 本指南详细介绍了使用 Prosody 在 Docker 中设置功能齐全的自托管 XMPP 服务器,为 Signal 等集中式消息应用程序提供注重隐私的替代方案。XMPP 的联合特性可防止厂商锁定,确保消息所有权和持久性。 设置涉及 Docker、Docker Compose、域名和 TLS 证书(可通过 Let’s Encrypt 轻松获得)。关键步骤包括配置 DNS 记录(用于发现的 SRV,用于文件上传的 A/CNAME)、使用 TLS 保护连接以及自定义 Prosody 的配置文件。 `carbons`(跨设备同步)、`smacks`(可靠传递)、`cloud_notify`(推送通知)和 `mam`(消息归档)等基本模块可增强移动体验。安全性是首要任务,强制加密并禁用公共注册。 通过 OMEMO 实现端到端加密,客户端如 Monal (iOS/macOS) 和 Conversations (Android) 支持此功能。语音和视频通话通过联合托管的 coturn 服务器实现 NAT 穿透。最后,必须调整防火墙规则以允许必要的端口。 尽管作者仍使用 Signal 进行日常通信,但他们认为这个自托管的 XMPP 服务器是一个有价值的、独立的的消息解决方案,提供控制权和面向未来的保障。

这个Hacker News讨论集中在寻找主流消息应用替代品的困难上。发帖者链接了一篇文章,内容是关于运行自托管XMPP服务器。 评论者分享了他们从不同平台迁移的经验。一位用户尝试了Matrix,但发现用户界面难以访问,特别是对于视力障碍人士,尽管Element X最近有所改进。然后他们转向Telegram,因为它易于使用且跨平台访问,但现在正在寻找替代方案,因为担心平台的拥有者。 另一位评论员指出托管聊天基础设施的复杂性,强调了除了处理大量连接(C10K+)之外的挑战。这场讨论强调了隐私、易用性和自托管与依赖集中式服务之间的权衡。
相关文章

原文

Notes from setting up Prosody in Docker for federated messaging, with file sharing, voice calls, and end-to-end encryption.

About a year ago I moved my personal messaging to Signal as part of a broader push to take ownership of my digital life. That went well. Most of my contacts made the switch, and I’m now at roughly 95% Signal for day-to-day conversations. But Signal is still one company running one service. If they shut down tomorrow or change direction, I’m back to square one.

XMPP fixes that. It’s federated, meaning your server talks to other XMPP servers automatically and you’re never locked into a single provider. Your messages live on your hardware. The protocol has been around since 1999 and it’s not going anywhere. I’d tried XMPP years ago and bounced off it, but the clients have come a long way since then. Monal and Conversations are genuinely nice to use now.

This post covers everything I did to get a fully working XMPP server running with Prosody in Docker, from DNS records through to voice calls.

Prerequisites

  • A server with Docker and Docker Compose
  • A domain you control
  • TLS certificates (Let’s Encrypt works well)

DNS records

XMPP uses SRV records to let clients and other servers find yours. You’ll need these in your DNS:

_xmpp-client._tcp.xmpp.example.com  SRV  0 5 5222 xmpp.example.com.
_xmpp-server._tcp.xmpp.example.com  SRV  0 5 5269 xmpp.example.com.

Port 5222 is for client connections, 5269 is for server-to-server federation. You’ll also want an A record pointing xmpp.example.com to your server’s IP.

If you want HTTP file uploads (I’d recommend it), add a CNAME or A record for upload.xmpp.example.com pointing to the same server. Same for conference.xmpp.example.com if you want group chats with a clean subdomain, though Prosody handles this internally either way.

TLS certificates

Prosody won’t start without certificates. I use Let’s Encrypt with the Cloudflare DNS challenge so I don’t need to expose port 80:

docker run --rm \
  -v ~/docker/xmpp/certs:/etc/letsencrypt \
  -v ~/docker/xmpp/cloudflare.ini:/etc/cloudflare.ini:ro \
  certbot/dns-cloudflare certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/cloudflare.ini \
  -d xmpp.example.com

The cloudflare.ini file contains your API token:

dns_cloudflare_api_token = your-cloudflare-api-token

After certbot runs, fix the permissions so Prosody can read the certs:

chmod -R 755 ~/docker/xmpp/certs/live/ ~/docker/xmpp/certs/archive/
chmod 644 ~/docker/xmpp/certs/archive/xmpp.example.com/*.pem

Set up a cron to renew monthly:

0 3 1 * * docker run --rm -v ~/docker/xmpp/certs:/etc/letsencrypt \
  -v ~/docker/xmpp/cloudflare.ini:/etc/cloudflare.ini:ro \
  certbot/dns-cloudflare renew \
  --dns-cloudflare-credentials /etc/cloudflare.ini \
  && docker restart xmpp

The Docker setup

The docker-compose.yml:

services:
  prosody:
    image: prosodyim/prosody:13.0
    container_name: xmpp
    restart: unless-stopped
    ports:
      - "5222:5222"
      - "5269:5269"
    volumes:
      - prosody-data:/var/lib/prosody
      - ./prosody.cfg.lua:/etc/prosody/prosody.cfg.lua:ro
      - ./certs/live/xmpp.example.com/fullchain.pem:/etc/prosody/certs/xmpp.example.com.crt:ro
      - ./certs/live/xmpp.example.com/privkey.pem:/etc/prosody/certs/xmpp.example.com.key:ro

volumes:
  prosody-data:

Two ports exposed: 5222 for clients, 5269 for federation. The data volume holds user accounts and message archives. Config and certs are mounted read-only.

Prosody configuration

This is the core of it. I’ll walk through the key sections rather than dumping the whole file.

Modules

Prosody is modular. My module list:

modules_enabled = {
    -- Core
    "roster"; "saslauth"; "tls"; "dialback"; "disco";
    "posix"; "ping"; "register"; "time"; "uptime"; "version";

    -- Security
    "blocklist";

    -- Multi-device & mobile
    "carbons"; "csi_simple";
    "smacks";         -- Stream Management (reliable delivery)
    "cloud_notify";   -- Push notifications for mobile

    -- Message archive
    "mam";

    -- User profiles & presence
    "vcard_legacy"; "pep"; "bookmarks";

    -- Admin
    "admin_shell";
}

The ones I found matter most for a good mobile experience: carbons syncs messages across all your devices instead of delivering to whichever one happened to be online. smacks (Stream Management) handles flaky connections gracefully, so messages aren’t lost when your phone briefly drops signal. cloud_notify enables push notifications so mobile clients don’t need a persistent connection, which is essential for battery life. And mam (Message Archive Management) stores history server-side for search and cross-device sync.

Security settings

c2s_require_encryption = true
s2s_require_encryption = true
s2s_secure_auth = true
authentication = "internal_hashed"
allow_registration = false

All connections are encrypted and registration is disabled since I create accounts manually with prosodyctl. I’ve enabled s2s_secure_auth, which means Prosody will reject connections from servers with self-signed or misconfigured certificates. You’ll lose federation with some poorly configured servers, but if you’re self-hosting for privacy reasons it doesn’t make much sense to relax authentication for other people’s mistakes.

OMEMO encryption

TLS encrypts connections in transit, but the server itself can still read your messages. If you’re self-hosting, that means you’re trusting yourself, which is fine. But if other people use your server, or if you just want the belt-and-braces approach, OMEMO adds end-to-end encryption so that not even the server operator can read message content.

OMEMO is built on the same encryption that Signal uses, so I’m comfortable trusting it. There’s nothing to configure on the server side either. OMEMO is handled entirely by the clients. Monal, Conversations, and Gajim all support it, and in most cases it’s enabled by default for new conversations. I’d recommend turning it on for everything and leaving it on.

Message archive

archive_expires_after = "1y"
default_archive_policy = true

Messages are kept for a year and archiving is on by default. Clients can opt out per-conversation if they want.

HTTP for file uploads

http_interfaces = { "*" }
http_ports = { 5280 }
https_ports = { }
http_external_url = "https://xmpp.example.com"

Prosody serves HTTP on port 5280 internally. I leave HTTPS to my reverse proxy (Caddy), which handles TLS termination. The http_external_url tells Prosody what URL to hand clients when they upload files.

Virtual host and components

VirtualHost "xmpp.example.com"
    ssl = {
        key = "/etc/prosody/certs/xmpp.example.com.key";
        certificate = "/etc/prosody/certs/xmpp.example.com.crt";
    }

Component "conference.xmpp.example.com" "muc"
    modules_enabled = { "muc_mam" }
    restrict_room_creation = "local"

Component "upload.xmpp.example.com" "http_file_share"
    http_file_share_size_limit = 10485760    -- 10 MB
    http_file_share_expires_after = 2592000  -- 30 days
    http_external_url = "https://xmpp.example.com"

The MUC (Multi-User Chat) component gives you group chats with message history via muc_mam. I restrict room creation to local users so random federated accounts can’t spin up rooms on my server.

The file share component handles image and file uploads. A 10 MB limit and 30-day expiry keeps disk usage under control.

Reverse proxy for file uploads

Prosody’s HTTP port needs to be reachable from the internet for file uploads to work. I use Caddy:

xmpp.example.com {
    reverse_proxy xmpp:5280
}

When a client sends an image, Prosody hands it a URL like https://xmpp.example.com/upload/... and the receiving client fetches it over HTTPS.

Creating accounts

With registration disabled, accounts are created from the command line:

docker exec -it xmpp prosodyctl adduser [email protected]

It prompts for a password. Done. Log in from any XMPP client.

Firewall

Open the XMPP ports:

sudo ufw allow 5222 comment 'XMPP client'
sudo ufw allow 5269 comment 'XMPP federation'

Port 80/443 for the reverse proxy if you haven’t already. If your server is behind a router, forward 5222 and 5269.

Voice and video calls

Text and file sharing work at this point. Voice and video calls need one more piece: a TURN/STUN server. Without it, clients behind NAT can’t establish direct media connections.

I run coturn alongside Prosody. The two share a secret, and Prosody generates temporary credentials for clients automatically.

Generate a shared secret:

The coturn docker-compose.yml:

services:
  coturn:
    image: coturn/coturn:latest
    container_name: coturn
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./turnserver.conf:/etc/coturn/turnserver.conf:ro
    tmpfs:
      - /var/lib/coturn

It runs with network_mode: host because TURN needs real network interfaces to handle NAT traversal. Docker’s port mapping breaks this.

The turnserver.conf:

listening-port=3478
tls-listening-port=5349
min-port=49152
max-port=49200
relay-threads=2
realm=xmpp.example.com
use-auth-secret
static-auth-secret=YOUR_SECRET_HERE
no-multicast-peers
no-cli
no-tlsv1
no-tlsv1_1
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
log-file=stdout

If your server is behind NAT, add:

external-ip=YOUR_PUBLIC_IP/YOUR_PRIVATE_IP

Then tell Prosody about it. Add "turn_external" to your modules, and inside the VirtualHost block:

    turn_external_host = "xmpp.example.com"
    turn_external_port = 3478
    turn_external_secret = "YOUR_SECRET_HERE"

Open the firewall ports:

sudo ufw allow 3478 comment 'STUN/TURN'
sudo ufw allow 5349 comment 'TURNS'
sudo ufw allow 49152:49200/udp comment 'TURN relay'

Verify with docker exec xmpp prosodyctl check turn.

Clients

On iOS I went with Monal, which is open source and supports all the modern XEPs. Push notifications work well. On Android, Conversations seems to be the go-to. On desktop, Gajim covers Linux and Windows, and Monal has a macOS build.

All of them support OMEMO encryption, file sharing, group chats, and voice/video calls.

Verifying your setup

Prosody has solid built-in diagnostics:

docker exec xmpp prosodyctl check

This checks DNS records, TLS certificates, connectivity, and module configuration. Fix anything it flags. The error messages are genuinely helpful.

The XMPP Compliance Tester is worth running too. Mine scored above 90% after getting the config right.

Final thoughts

The whole setup runs in two small Docker containers and a reverse proxy entry. Prosody, file uploads, message archive, push notifications, group chats, voice calls.

I still use Signal for most day-to-day conversations and I’m not planning to stop. But having my own XMPP server means I’m not entirely dependent on any single service. I can message anyone on any XMPP server, not just people who signed up to the same one. It’s a nice fallback to have.

If you’re already running Docker on a server somewhere, it’s a good weekend project.

联系我们 contact @ memedata.com