Jabber Calls in a Docker Container in 5 Minutes
A step-by-step guide to deploying your own XMPP server with encrypted audio and video calls using Snikket in Docker, including TURN server setup to disguise voice traffic as HTTPS.
The idea is simple: deploy your own XMPP server (Jabber) with support for encrypted calls. The solution uses Snikket — an open platform for private communications that runs in Docker containers.
What You'll Need
- A VDS/VPS with a public IP address
- A domain name
Step 1: Install Docker and Docker Compose v2
For Ubuntu/Debian:
sudo apt update
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now dockerFor RHEL/Rocky Linux/AlmaLinux:
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now dockerFor CentOS 7:
yum install yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum makecache fast
yum install docker-ce docker-ce-cli containerd.io
systemctl enable --now dockerStep 2: Configure DNS
You need to create one A record and two CNAMEs:
A chat.your-domain.com → <public IP of VDS/VPS>
CNAME groups.chat.your-domain.com → chat.your-domain.com
CNAME share.chat.your-domain.com → chat.your-domain.comStep 3: Open Ports
Ubuntu/Debian:
# Web/ACME
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# XMPP
sudo ufw allow 5222/tcp # c2s (client-to-server)
sudo ufw allow 5269/tcp # s2s (server-to-server, optional)
# STUN/TURN
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 3479/tcp
sudo ufw allow 3479/udp
sudo ufw allow 5349/tcp # TLS
sudo ufw allow 5349/udp
sudo ufw allow 5350/tcp
sudo ufw allow 5350/udp
# RTP relay for TURN (media)
sudo ufw allow 49152:65535/udp
# Optional: proxy65 for file transfers
sudo ufw allow 5000/tcpRHEL/Rocky Linux/AlmaLinux:
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --permanent --add-port=5222/tcp
sudo firewall-cmd --permanent --add-port=5269/tcp
sudo firewall-cmd --permanent --add-port=3478/tcp
sudo firewall-cmd --permanent --add-port=3478/udp
sudo firewall-cmd --permanent --add-port=3479/tcp
sudo firewall-cmd --permanent --add-port=3479/udp
sudo firewall-cmd --permanent --add-port=5349/tcp
sudo firewall-cmd --permanent --add-port=5349/udp
sudo firewall-cmd --permanent --add-port=5350/tcp
sudo firewall-cmd --permanent --add-port=5350/udp
sudo firewall-cmd --permanent --add-port=49152-65535/udp
sudo firewall-cmd --permanent --add-port=5000/tcp
sudo firewall-cmd --reloadStep 4: Install Snikket
cd /opt
git clone https://github.com/snikket-im/snikket-selfhosted.git snikket
cd snikket
./scripts/init.shThe script will ask for your domain name and admin email, then:
./scripts/start.shAfter a successful launch, a login form will be available at https://<your-domain>/.
Create an admin invite:
./scripts/new-invite.sh --admin --group defaultStep 5: Admin Panel and Invitations
After registering the administrator, the admin zone is available at https://<your-domain>/, where you can send invitations to users.
Included Features
- XMPP Server — a private messenger with no ads
- Federation — interoperability with other XMPP servers
- Group Chats — private and public rooms
- Multimedia — file and photo sharing
- WebRTC Calls — built-in STUN/TURN for audio and video
- E2E Encryption — end-to-end encryption independent of phone numbers
- Multi-device — one user across multiple devices
- Docker Deployment — quick setup
- Minimal Resources — 1 GB RAM is enough, runs on Raspberry Pi
- Let's Encrypt — automatic certificate management
- Jitter Buffer — audio delay smoothing on unstable networks
- Free to Use — or paid Snikket hosting (~$6/month)
Technical Details of Calls
P2P Mechanism:
- By default, calls establish a direct connection via STUN
- With complex NAT (symmetric, CGNAT), the TURN server kicks in to relay UDP packets
- The media stream is encrypted using the SRTP (Secure RTP) protocol
- Even the server administrator cannot eavesdrop on calls — they don't have the decryption keys
Disguising Audio Traffic as HTTPS
If your provider blocks calls, you can run TURN on port 443 using two methods:
Option A: Two Public IPs
Snikket on IP1, TURN on IP2.
DNS:
chat.your-domain.com → IP1
turn.your-domain.com → IP2Configuration /opt/snikket/snikket.conf:
SNIKKET_DOMAIN=chat.example.com
SNIKKET_ADMIN_EMAIL=admin@example.com
SNIKKET_LETSENCRYPT_TOS_AGREE=y
SNIKKET_TWEAK_TURNSERVER=1
SNIKKET_TWEAK_TURNSERVER_DOMAIN=turn.example.com
SNIKKET_TWEAK_TURNSERVER_SECRET=CHANGE_ME_SECRET
SNIKKET_TWEAK_EXTRA_CONFIG=/config/prosody-external.cfg.luaFile /opt/snikket/prosody-external.cfg.lua:
if modules_enabled then
Lua.table.insert(modules_enabled, "external_services")
else
modules_enabled = { "external_services" }
end
external_services = external_services or {}
Lua.table.insert(external_services, {
type = "turn",
transport = "udp",
host = "turn.example.com",
port = 3478,
secret = "CHANGE_ME_SECRET_primary_001",
})
Lua.table.insert(external_services, {
type = "turn",
transport = "tcp",
host = "turn.example.com",
port = 443,
secret = "CHANGE_ME_SECRET_primary_001",
})Docker Compose for a separate TURN server:
services:
coturn:
image: coturn/coturn:latest
container_name: coturn
network_mode: host
user: "0:0"
restart: unless-stopped
volumes:
- /opt/turnserver/turnserver.conf:/etc/turnserver/turnserver.conf:ro
- /etc/letsencrypt/live/turn.example.com/fullchain.pem:/etc/ssl/certs/turn.pem:ro
- /etc/letsencrypt/live/turn.example.com/privkey.pem:/etc/ssl/private/turn.key:ro
command: ["-c", "/etc/turnserver/turnserver.conf", "--no-cli"]
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"Configuration /opt/turnserver/turnserver.conf:
realm=chat.example.com
server-name=turn.example.com
use-auth-secret
static-auth-secret=CHANGE_ME_SECRET
fingerprint
listening-port=3478
tls-listening-port=443
listening-ip=0.0.0.0
min-port=49160
max-port=49220
cert=/etc/ssl/certs/turn.pem
pkey=/etc/ssl/private/turn.key
simple-log
no-stdout-logObtaining a certificate:
certbot certonly --standalone -d turn.example.com --agree-tos -m admin@example.com --no-eff-emailOption B: Single IP with nginx Splitter
Both services (Snikket and TURN) on one IP, with nginx splitting traffic by SNI.
Nginx configuration:
stream {
map $ssl_preread_server_name $backend {
turn.example.com turn_tls;
default https_upstream;
}
upstream turn_tls { server 127.0.0.1:5349; }
upstream https_upstream { server 127.0.0.1:8443; }
server {
listen 443 reuseport;
proxy_pass $backend;
ssl_preread on;
}
}
server {
listen 80; listen [::]:80;
server_name chat.your-domain.com groups.chat.your-domain.com share.chat.your-domain.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:5080;
}
}Configuration /etc/coturn/turnserver.conf:
listening-ip=127.0.0.1
tls-listening-port=5349
relay-ip=<PUBLIC_IP>
fingerprint
use-auth-secret
static-auth-secret=<LONG_SECRET>
realm=example.com
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pemAdvantages: single IP, traffic is disguised as HTTPS.
Disadvantages: more complex setup, slight increase in latency due to nginx.
Verifying TURN on Port 443
openssl s_client -connect turn.example.com:443 -briefExpected result:
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN = turn.example.com
Hash used: SHA256
Signature type: RSA-PSS
Verification: OK
Server Temp Key: X25519, 253 bitsOn the server:
ss -ltnp | egrep ':443|:3478' || trueYou should see listening sockets for turnserver.
FAQ
What is this article about in one sentence?
This article explains the core idea in practical terms and focuses on what you can apply in real work.
Who is this article for?
It is written for engineers, technical leaders, and curious readers who want a clear, implementation-focused explanation.
What should I read next?
Use the related articles below to continue with closely connected topics and concrete examples.