Setting up an Ejabberd XMPP server with Video Calls and Multimedia support
Introduction
XMPP (Extensible Messaging and Presence Protocol) is something that has always caught my attention. I will always wonder why it never became mainstream and the de-facto standard for Instant Messaging, given its capabilities and extensibility. I have been testing and using XMPP since ~2013, but of course I had always used public servers, never setting up my own server. Last December, while the Christmas holidays were taking place, I decided to set up my own XMPP server (who said that it was not fun for the whole family?).
Note: big kudos to the Ejabberd’s XMPP room for all the help and guidance they provided me while setting up the server!
Choosing the XMPP server software
There are several well-known XMPP server implementations available, such as Ejabberd, Prosody, and Openfire. I decided to go with Ejabberd basically because the best servers I had used in the past were running Ejabberd (conversations.im was my reference server). Additionally, it has a very active community, good documentation, and has been very well maintained over the years.
Setting up Ejabberd with Docker and PostgreSQL
Here’s where the pain starts. While Ejabberd provides an official Docker image and great documentation, now you need to start checking which module, with which options, and with which dependencies you need to make a certain XEP (XMPP Extension Protocol) work. In my case, I wanted to have Calls, Video Calls, File Transfer support, status indicators for uses (e.g., typing, online, offline), mesage delivery receipts, and other features that are expected from a modern IM platform. After a few hours, I got a server that was at least being able to send and receive messages, but I couldn’t send files or make calls.
Trying to find a working configuration for Ejabberd with all the required modules, I searched for “ejabberd.yml configuration file” and to my surprise, the Ejabberd repository already provides an example file that already works very well. With that on my hands and the experience from the previous attempts, I just had to tweak a few options to make it work the way I wanted.
Note: from now on, replace jabber.edu4rdshl.dev with your own domain, IP addresses with your own ones, and set strong passwords for the admin user and the PostgreSQL database. I will also assume that you’re using a folder named ejabberd in your home directory to store all the required files.
The ejabberd.yml file
Here’s the ejabberd.yml file that I ended up using:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
hosts:
- "jabber.edu4rdshl.dev"
loglevel: 4
acl:
admin:
user: "admin@jabber.edu4rdshl.dev"
local:
user_regexp: ""
access_rules:
local:
allow: local
c2s:
deny: blocked
allow: all
announce:
allow: admin
configure:
allow: admin
muc_create:
allow: local
pubsub_createnode:
allow: local
trusted_network:
allow: loopback
## PostgreSQL Configuration
sql_type: pgsql
sql_server: postgres
sql_port: 5432
sql_database: ejabberd
sql_username: ejabberd
sql_password: SuperSecretPostgresPassword
default_db: sql
# Global S2S settings
s2s_use_starttls: required
s2s_ciphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
s2s_protocol_options:
- no_sslv2
- no_sslv3
- no_tlsv1
- no_tlsv1_1
# Global C2S settings
c2s_protocol_options:
- no_sslv2
- no_sslv3
- no_tlsv1
- no_tlsv1_1
c2s_ciphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
# Listening ports
listen:
- port: 5222
ip: "::"
module: ejabberd_c2s
shaper: c2s_shaper
access: c2s
max_stanza_size: 262144
starttls_required: true
- port: 5223
ip: "::"
module: ejabberd_c2s
shaper: c2s_shaper
access: c2s
max_stanza_size: 262144
tls: true
- port: 5269
ip: "::"
module: ejabberd_s2s_in
max_stanza_size: 524288
shaper: s2s_shaper
- port: 5270
ip: "::"
module: ejabberd_s2s_in
max_stanza_size: 524288
shaper: s2s_shaper
tls: true
- port: 5280
ip: "::"
module: ejabberd_http
request_handlers:
/admin: ejabberd_web_admin
- port: 5443
ip: "::"
module: ejabberd_http
tls: true
request_handlers:
/api: mod_http_api
/bosh: mod_bosh
/captcha: ejabberd_captcha
/upload: mod_http_upload
/ws: ejabberd_http_ws
/.well-known/host-meta: mod_host_meta
/.well-known/host-meta.json: mod_host_meta
- port: 5478
ip: "::"
transport: udp
module: ejabberd_stun
use_turn: true
turn_min_port: 49152
turn_max_port: 50000
## The server's public IPv4 address:
turn_ipv4_address: "158.69.216.228"
## The server's public IPv6 address:
turn_ipv6_address: "2607:5300:205:200::6ac7"
certfiles:
- "/etc/letsencrypt/live/jabber.edu4rdshl.dev/privkey.pem"
- "/etc/letsencrypt/live/jabber.edu4rdshl.dev/fullchain.pem"
# Module configuration
modules:
mod_adhoc: {}
mod_adhoc_api: {}
mod_admin_extra: {}
mod_announce:
access: announce
mod_avatar: {}
mod_blocking: {}
mod_bosh: {}
mod_caps: {}
mod_carboncopy: {}
mod_client_state: {}
mod_configure: {}
mod_disco: {}
mod_host_meta:
bosh_service_url: "https://@HOST@:5443/bosh"
websocket_url: "wss://@HOST@:5443/ws"
mod_http_api: {}
mod_http_upload:
put_url: "https://@HOST@:5443/upload"
docroot: /opt/ejabberd/upload
max_size: 524288000
custom_headers:
"Access-Control-Allow-Origin": "https://@HOST@"
"Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS"
"Access-Control-Allow-Headers": "Content-Type"
mod_last: {}
mod_mam:
db_type: sql
assume_mam_usage: true
default: always
mod_muc:
access:
- allow
access_admin:
- allow: admin
access_create: muc_create
access_mam:
- allow
access_persistent: muc_create
default_room_options:
mam: true
max_users: 10000
mod_muc_admin: {}
mod_muc_occupantid: {}
mod_offline:
access_max_user_messages: max_user_offline_messages
mod_ping: {}
mod_privacy: {}
mod_private: {}
mod_proxy65:
access: local
max_connections: 5
mod_pubsub:
access_createnode: all
plugins:
- flat
- pep
force_node_config:
## Avoid buggy clients to make their bookmarks public
storage:bookmarks:
access_model: whitelist
mod_push: {}
mod_roster:
versioning: true
mod_s2s_bidi: {}
mod_s2s_dialback: {}
mod_shared_roster: {}
mod_stream_mgmt:
resend_on_timeout: if_offline
mod_stun_disco: {}
mod_vcard: {}
mod_vcard_xupdate: {}
mod_version:
show_os: false
The SSL certificates
It’s highly recommended to use valid SSL certificates for your XMPP server, as most XMPP clients will refuse to connect to servers with self-signed or invalid certificates. You can use Let’s Encrypt to obtain free SSL certificates for your domain. Make sure to mount the certificate files into the Docker container, as shown in the docker-compose.yml file below.
To automate the process of obtaining and renewing Let’s Encrypt certificates, you can use Certbot, which even has DNS plugins for most popular DNS providers (in cases where you can’t or don’t want to bind port 80 on your server). In my case, I used the Cloudflare DNS plugin to obtain the certificates. You need a Cloudflare API token with permissions to manage DNS records for your domain, and then create a file named cloudflare-dns.ini inside the current folder where you are working at. It needs to use the following format:
1
2
# Cloudflare API token used by Certbot
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN_HERE
After that, you can run the following command to obtain the certificates:
1
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/ejabberd/cloudflare-dns.ini -d jabber.edu4rdshl.dev -d "*.jabber.edu4rdshl.dev"
That will create the required certificates, and also create an automatic renewal process using cron jobs. After each renewal, you need to reload the Ejabberd server to make it use the new certificates. You can do that by adding a deploy hook in the /etc/letsencrypt/renewal-hooks/deploy directory with the following content:
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# Fix ownership so ejabberd user can read the new certs
chown -R 9000:9000 /etc/letsencrypt/live/jabber.edu4rdshl.dev
chown -R 9000:9000 /etc/letsencrypt/archive/jabber.edu4rdshl.dev
# Reload ejabberd to apply the new certificate
docker compose -f /home/ubuntu/ejabberd/docker-compose.yml exec ejabberd ejabberdctl reload_config
echo "$(date) - Certbot deploy hook: Completed successfully" >> /var/log/certbot-deploy.log
Docker dual-stack networking (IPv4 and IPv6)
As we are going to use both IPv4 and IPv6 on our XMPP server, we need to configure Docker to work with both IPv4 and IPv6 inside the containers. Add the following configuration to your Docker daemon configuration file, usually located at /etc/docker/daemon.json:
1
2
3
4
{
"ipv6": true,
"fixed-cidr-v6": "fd12:3456:789a:1::/64"
}
The docker-compose.yml file
The docker image requires a few volumes to persist data, in addition to a PostgreSQL (if you want to use it - recommended) as the database. You can use the official PostgreSQL Docker image for that.
Here’s how the full docker-compose.yml file looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
services:
ejabberd:
image: ghcr.io/processone/ejabberd:latest
container_name: ejabberd
restart: unless-stopped
depends_on:
- postgres
volumes:
- ./ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml:ro
- ejabberd_database:/opt/ejabberd/database
- ejabberd_logs:/opt/ejabberd/logs
- ejabberd_upload:/opt/ejabberd/upload
- /etc/letsencrypt/live/jabber.edu4rdshl.dev:/etc/letsencrypt/live/jabber.edu4rdshl.dev:ro
- /etc/letsencrypt/archive/jabber.edu4rdshl.dev:/etc/letsencrypt/archive/jabber.edu4rdshl.dev:ro
ports:
- "5222:5222" # C2S STARTTLS
- "5223:5223" # C2S TLS
- "5269:5269" # S2S STARTTLS
- "5270:5270" # S2S TLS
- "5280:5280" # HTTP (admin – preferably an internal bind, but published if needed)
- "5443:5443" # HTTPS (API, BOSH, upload, WS, etc.)
- "5478:5478/udp" # STUN/TURN UDP
environment:
- EJABBERD_MACRO_HOST=jabber.edu4rdshl.dev
- REGISTER_ADMIN_PASSWORD=SuperSecretAdminPassword
networks:
- ejabberd_net
postgres:
image: postgres:18-alpine
container_name: ejabberd_postgres
restart: unless-stopped
environment:
POSTGRES_DB: ejabberd
POSTGRES_USER: ejabberd
POSTGRES_PASSWORD: SuperSecretPostgresPassword
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- ejabberd_net
networks:
ejabberd_net:
enable_ipv6: true
volumes:
postgres_data:
ejabberd_database:
ejabberd_logs:
ejabberd_upload:
The firewall configuration
Depending on what you want to achieve, you need to open the required ports in your firewall. In my case, I opened the following ports:
5222/tcp: for C2S connections with STARTTLS5223/tcp: for C2S connections with TLS5269/tcp: for S2S connections with STARTTLS5270/tcp: for S2S connections with TLS5443/tcp: for HTTPS connections (API, BOSH, upload, WS, etc.)5478/udp: for STUN/TURN UDP49152-50000/udp: for TURN media relay
Additionally I have port 5280/tcp open, but only for my wireguard VPN network.
So, the sudo ufw status command shows something like this:
1
2
3
4
5
6
7
8
9
10
11
12
Status: active
To Action From
-- ------ ----
5222/tcp ALLOW Anywhere
5223/tcp ALLOW Anywhere
5269/tcp ALLOW Anywhere
5270/tcp ALLOW Anywhere
5443/tcp ALLOW Anywhere
5478/udp ALLOW Anywhere
49152:50000/udp ALLOW Anywhere
5280/tcp on wg0 ALLOW IN Anywhere
# also the same rules for IPv6
The DNS configuration
You need to set up the required DNS records for your XMPP server to work properly. At a minimum, you need to set up an A record for your domain pointing to your server’s IP address and optionally an AAAA record if you have an IPv6 address. Additionally, you might want to set up the following SRV records:
1
2
3
4
5
6
_xmpp-client._tcp.jabber.edu4rdshl.dev. 3600 IN SRV 5 0 5222 jabber.edu4rdshl.dev.
_xmpp-server._tcp.jabber.edu4rdshl.dev. 3600 IN SRV 5 0 5269 jabber.edu4rdshl.dev.
_xmpps-client._tcp.jabber.edu4rdshl.dev. 3600 IN SRV 5 0 5223 jabber.edu4rdshl.dev.
_xmpps-server._tcp.jabber.edu4rdshl.dev. 3600 IN SRV 5 0 5270 jabber.edu4rdshl.dev.
_stun._udp.jabber.edu4rdshl.dev. 3600 IN SRV 5 0 5478 jabber.edu4rdshl.dev.
_turn._udp.jabber.edu4rdshl.dev. 3600 IN SRV 5 0 5478 jabber.edu4rdshl.dev.
Testing the server
After setting up everything, you can start the Ejabberd server using Docker Compose:
1
docker compose up -d
Now, you can access your admin panel at https://your_private_ip:5443/admin using the admin user and password you set in the docker-compose.yml file and create a new user for testing. After that, you can use an XMPP client like Conversations (Android) or Gajim (Linux, Windows) to connect to your server and test its functionality. If everything is set up correctly, you should be able to send messages, make calls, and transfer files between users and other XMPP servers.
Conclusion
While it took me some time to set up everything correctly, having my own XMPP server with Video Calls and Multimedia support has been a rewarding experience. Ejabberd’s flexibility and extensibility make it a great choice for anyone looking to set up their own XMPP server. I hope that you enjoy this straightforward guide to set up your own Ejabberd server using Docker and PostgreSQL!
Happy chatting!
