Why XMPP for Secure Communications?
XMPP (Extensible Messaging and Presence Protocol) has been around since 1999 and it’s still the best option for decentralized, real-time messaging when you care about data sovereignty. When your team relies on third-party platforms — Slack, Teams, WhatsApp — your messages, metadata, and contact lists all live on someone else’s servers. For organizations with strict OPSEC requirements, that’s simply not acceptable.
Running your own XMPP server means you control everything: user accounts, message routing, encryption keys, and data retention. Nobody else has access unless you grant it.
Why Prosody?
There are several XMPP server implementations out there — ejabberd, OpenFire, MongooseIM — but Prosody stands out for self-hosted deployments because it’s genuinely lightweight. It’s written in Lua, has a clean modular architecture, and won’t eat your server’s resources. More importantly, it has a small attack surface compared to larger, more feature-bloated alternatives.
The module system is one of its biggest strengths. You enable only what you actually need, which means fewer moving parts that can go wrong.
Why Docker for This?
Containerizing Prosody gives you some real operational advantages. The process runs in isolation from your host system, so even if something goes wrong with the application, the damage is contained. Deployments become reproducible — you can spin up an identical environment on another machine in minutes. And when a new Prosody version drops, updating is just a pull and restart rather than a full package management headache.
Infrastructure Requirements
How much hardware you need depends on how many people will be connecting:
| User Capacity | CPU | RAM | Storage |
|---|---|---|---|
| Small (up to 100 users) | 1 vCPU | 1 GB | 10 GB SSD |
| Medium (up to 500 users) | 2 vCPU | 2 GB | 20 GB SSD |
| Large (1000+ users) | 4 vCPU | 4 GB | 50 GB SSD |
These numbers assume typical messaging workloads. If you’re running heavy group chats with lots of file transfers, scale up accordingly.
Step-by-Step Deployment
1. Pick Your OS
Go with Debian 12 (Bookworm). It’s stable, well-supported, and the minimal install keeps unnecessary packages — and their vulnerabilities — off your system.
2. Harden the Firewall First
Before anything else, lock down the network. UFW makes this straightforward:
apt update && apt upgrade -y
apt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh # Ideally restrict this to your management IP
ufw allow 5222/tcp # Client-to-server connections
ufw allow 5269/tcp # Server-to-server federation
ufw allow 80,443/tcp # Required for Let's Encrypt certificate issuance
ufw enable
The default-deny incoming policy means nothing gets through unless you’ve explicitly allowed it. Start there.
3. Install Docker
Pull Docker from the official repositories:
apt install -y docker.io docker-compose
systemctl enable --now docker
4. Write the Docker Compose File
Create a docker-compose.yml that maps the important directories to the host so your data and config survive container restarts:
version: '3.8'
services:
prosody:
image: prosody/prosody:latest
container_name: prosody
restart: unless-stopped
ports:
- "5222:5222"
- "5269:5269"
volumes:
- ./data:/var/lib/prosody
- ./config:/etc/prosody
- ./certs:/etc/letsencrypt:ro
environment:
- XMPP_DOMAIN=example.com
security_opt:
- no-new-privileges:true
The no-new-privileges flag prevents any process inside the container from escalating its permissions — a simple but effective hardening step.
5. Get Your TLS Certificate
Running XMPP without encryption is a bad idea. Let’s Encrypt gives you a trusted certificate for free:
apt install certbot
certbot certonly --standalone -d example.com
Then point Prosody at those certificates in config/prosody.cfg.lua:
ssl = {
key = "/etc/letsencrypt/live/example.com/privkey.pem";
certificate = "/etc/letsencrypt/live/example.com/fullchain.pem";
ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
}
Explicitly listing strong ciphers prevents clients from negotiating weaker ones — it closes the door on downgrade attacks.
6. Start the Server
docker-compose up -d
Watch the startup logs to confirm the certificates loaded correctly and all modules initialized without errors:
docker logs -f prosody
Managing Users
Adding Accounts
Use prosodyctl inside the container to create user accounts:
docker exec -it prosody prosodyctl adduser user@example.com
Day-to-Day Admin
- Remove a user:
docker exec -it prosody prosodyctl deluser user@example.com - Change a password:
docker exec -it prosody prosodyctl passwd user@example.com
Choosing a Client
Your server is only as useful as the clients connecting to it. For end-to-end encryption, you want clients that support OMEMO — it’s the gold standard for XMPP E2E encryption.
- Desktop (Windows/macOS/Linux): Gajim or Dino are both solid choices
- Android: Conversations is the go-to
- iOS: Monal is actively maintained and works well
Once installed, connect with your full Jabber ID (user@example.com), enter your credentials, and make sure OMEMO is turned on before you start sending anything sensitive.
Keeping It Running
Auto-Renew Certificates
Let’s Encrypt certificates expire every 90 days. Set up a cron job so you never have to think about it:
crontab -e
Add this line:
0 0 * * * certbot renew --quiet && docker restart prosody
Staying Up to Date
Pull the latest Prosody image regularly to pick up security patches:
docker-compose pull && docker-compose up -d
Watch Your Logs
Keep an eye out for brute-force attempts or unusual federation traffic:
docker logs --tail 100 prosody
Advanced Configuration
Scaling
If your user base grows, you have two main options: add more CPU and RAM to handle more concurrent connections, or offload user authentication to a PostgreSQL database instead of flat files. The database approach is more resilient at scale.
Useful Modules
These modules are worth enabling in prosody.cfg.lua for most deployments:
modules_enabled = {
"muc"; -- Multi-user chat rooms
"mam"; -- Message archive management (local message history)
"csi_simple"; -- Client state indication (better mobile battery life)
"smacks"; -- Stream management (prevents message loss on flaky connections)
}
Admin Interface
If you want a web-based admin panel, the Prosody Web Admin module works well — just make sure you put it behind an Nginx reverse proxy with HTTP basic auth and IP allowlisting. Never expose admin interfaces directly to the internet.
Wrapping Up
Self-hosting Prosody on Docker gives you a genuinely private messaging infrastructure that you own and control. The setup isn’t complicated, and once it’s running, maintenance is mostly automated. With proper TLS, a tight firewall, and regular updates, you have a communications platform that’s resistant to surveillance and doesn’t hand your data to a third party.