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 docker

For 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 docker

For 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 docker

Step 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.com

Step 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/tcp

RHEL/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 --reload

Step 4: Install Snikket

cd /opt
git clone https://github.com/snikket-im/snikket-selfhosted.git snikket
cd snikket
./scripts/init.sh

The script will ask for your domain name and admin email, then:

./scripts/start.sh

After a successful launch, a login form will be available at https://<your-domain>/.

Create an admin invite:

./scripts/new-invite.sh --admin --group default

Step 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

  1. XMPP Server — a private messenger with no ads
  2. Federation — interoperability with other XMPP servers
  3. Group Chats — private and public rooms
  4. Multimedia — file and photo sharing
  5. WebRTC Calls — built-in STUN/TURN for audio and video
  6. E2E Encryption — end-to-end encryption independent of phone numbers
  7. Multi-device — one user across multiple devices
  8. Docker Deployment — quick setup
  9. Minimal Resources — 1 GB RAM is enough, runs on Raspberry Pi
  10. Let's Encrypt — automatic certificate management
  11. Jitter Buffer — audio delay smoothing on unstable networks
  12. 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 → IP2

Configuration /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.lua

File /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-log

Obtaining a certificate:

certbot certonly --standalone -d turn.example.com --agree-tos -m admin@example.com --no-eff-email

Option 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.pem

Advantages: 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 -brief

Expected 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 bits

On the server:

ss -ltnp | egrep ':443|:3478' || true

You 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.