How to Install ejabberd and a TURN Server (coturn) on Ubuntu for Video Calls
Run your own chat and video call server: ejabberd 24.12, MySQL, a free SSL certificate and the coturn TURN server on Ubuntu, step by step, with the ports, tests and common fixes.
ejabberd is an XMPP chat server you run on your own VPS. It handles sign-up, login, one-to-one chat, group rooms and the call signalling for an Android chat app. A TURN server sits next to it and relays the voice and video when two phones cannot reach each other directly, which happens a lot on mobile data.
With both on your own server you pay for the VPS and nothing else: no per-minute call fees, no per-user charges, and the messages stay in your own database. This guide sets up ejabberd 24.12, MySQL, a free SSL certificate and the coturn TURN server on Ubuntu, step by step.
What you will set up
| Part | What it does |
|---|---|
| ejabberd 24.12 | The chat server: accounts, messages, rooms, call signalling |
| MySQL | Stores users, contacts, rooms and offline messages |
| Let’s Encrypt certificate | Encrypted connections; apps refuse servers without a valid one |
| coturn (port 3478) | The TURN server: relays the voice and video of calls |
| mod_pottymouth | Bad-word filter: blocked words in messages arrive as **** |
| mod_default_rooms | New users join your group rooms automatically after sign-up |
| Shared roster group | All users, or only online users, appear in everyone's contact list |
Why ejabberd 24.12 exactly
- The configuration below is written for 24.12. Newer releases renamed some options (for example
new_sql_schemabecamesql_schema_multihostin 25.10), so a config that works on one version can stop ejabberd from starting on another. - Our chat app source codes are built and tested on 24.12. Using the same version means the app and the server behave the way they did in testing.
- Do not use
apt install ejabberd. Ubuntu’s own package is a different version with different paths. Install the official 24.12 package from ProcessOne instead, as shown in Step 3.
What you need
- A VPS with Ubuntu 22.04 or 24.04, a public IPv4 address and root access
- 2 GB RAM is comfortable; 1 GB works for testing
- A domain or subdomain for the chat server, for example
chat.yourdomain.com - Enough monthly bandwidth: calls that go through TURN use your VPS traffic
In the commands below, replace chat.yourdomain.com with your domain and 203.0.113.10 with your server’s public IP.
Step 1: Point your domain at the server
In your domain’s DNS, add an A record: name chat, value your server’s IP. Wait a few minutes, then check it from the server:
ping -c 2 chat.yourdomain.com
The reply must show your server’s IP. The SSL certificate in Step 4 cannot be issued until it does.
Step 2: Install MySQL and create the database
sudo apt update
sudo apt install -y mysql-server
sudo mysql
Inside MySQL, create the database and a user for ejabberd:
CREATE DATABASE ejabberd CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'ejabberd'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON ejabberd.* TO 'ejabberd'@'localhost';
FLUSH PRIVILEGES;
EXIT;
You do not need to import any tables. Since version 24.06, ejabberd creates and updates its own tables on the first start (the update_sql_schema option is on by default). If you prefer to create them by hand, the file is /opt/ejabberd-24.12/lib/ejabberd-24.12.0/priv/sql/mysql.sql after Step 3.
Step 3: Download and install ejabberd 24.12
Download the official package from the ejabberd releases on GitHub and install it:
cd /tmp
wget https://github.com/processone/ejabberd/releases/download/24.12/ejabberd_24.12-1_amd64.deb
sudo dpkg -i ./ejabberd_24.12-1_amd64.deb
This is the package for 64-bit Intel and AMD servers (amd64), which is what almost every VPS uses. It contains its own Erlang, so nothing else needs installing. The package:
- creates a system user called
ejabberd - puts the program in
/opt/ejabberd-24.12 - puts your configuration, logs and data in
/opt/ejabberd, with the config at/opt/ejabberd/conf/ejabberd.yml
After the install, run these so ejabberd starts with the server and the ejabberdctl command works:
sudo cp /lib/systemd/system/ejabberd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ejabberd
echo 'export PATH="/opt/ejabberd-24.12/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Check that it is running:
sudo ejabberdctl status
You should see that the node is started and ejabberd is running. The package installs the file directly, not from an apt repository, so a normal apt upgrade will not move you to another version.
Step 4: Get a free SSL certificate
Apps connect with TLS, and they refuse a server whose certificate does not match its domain. Get a free Let’s Encrypt certificate. Certbot needs port 80 open for a moment:
sudo ufw allow 80/tcp
sudo apt install -y certbot
sudo certbot certonly --standalone -d chat.yourdomain.com
Copy the certificate where ejabberd can read it:
sudo mkdir -p /opt/certs/xmpp
sudo cp /etc/letsencrypt/live/chat.yourdomain.com/fullchain.pem /etc/letsencrypt/live/chat.yourdomain.com/privkey.pem /opt/certs/xmpp/
sudo chown -R ejabberd:ejabberd /opt/certs
sudo chmod 600 /opt/certs/xmpp/privkey.pem
Let’s Encrypt certificates last 90 days and certbot renews them by itself. To copy each renewed certificate for ejabberd too, create a renewal hook:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/ejabberd.sh
#!/bin/sh
cp /etc/letsencrypt/live/chat.yourdomain.com/fullchain.pem /etc/letsencrypt/live/chat.yourdomain.com/privkey.pem /opt/certs/xmpp/
chown ejabberd:ejabberd /opt/certs/xmpp/*.pem
ejabberdctl reload_config
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/ejabberd.sh
Step 5: Configure ejabberd
Open the configuration file. It is YAML, so indent with spaces, never tabs, and keep the indentation exactly as shown:
sudo nano /opt/ejabberd/conf/ejabberd.yml
Your domain, certificate and database. Set these near the top:
hosts:
- chat.yourdomain.com
certfiles:
- "/opt/certs/xmpp/*.pem"
host_config:
chat.yourdomain.com:
auth_method: sql
default_db: sql
sql_type: mysql
sql_server: "127.0.0.1"
sql_port: 3306
sql_database: "ejabberd"
sql_username: "ejabberd"
sql_password: "a-long-random-password"
The listeners. In the listen: section, keep the client port with TLS required, the HTTPS port for the web admin and file uploads, and add ejabberd’s TURN listener:
listen:
-
port: 5222
ip: "::"
module: ejabberd_c2s
max_stanza_size: 262144
shaper: c2s_shaper
access: c2s
starttls_required: true
-
port: 5443
ip: "::"
module: ejabberd_http
tls: true
request_handlers:
/admin: ejabberd_web_admin
/upload: mod_http_upload
/ws: ejabberd_http_ws
-
port: 3478
transport: udp
module: ejabberd_stun
use_turn: true
turn_ipv4_address: "203.0.113.10"
turn_min_port: 10000
turn_max_port: 20000
turn_ipv4_address must be your server’s public IP, or calls will fail on mobile data. Older guides call this option turn_ip; 24.12 still accepts that name and changes it to turn_ipv4_address by itself.
The admin and the modules. Make your admin account an admin, and check these modules are in the modules: section:
acl:
admin:
user:
- "admin@chat.yourdomain.com"
modules:
mod_register:
ip_access: all
mod_stun_disco: {}
mod_muc:
access_create: muc_create
default_room_options:
persistent: true
mod_http_upload:
docroot: "/opt/ejabberd/upload"
put_url: "https://@HOST@:5443/upload"
max_size: 104857600
mod_http_upload_quota:
max_days: 7
mod_shared_roster:
db_type: sql
cache_life_time: 1
| Module | Why the app needs it |
|---|---|
mod_register | People can create an account from inside the app |
mod_stun_disco | Tells apps about the TURN listener and gives them a short-lived login for it |
mod_muc | Group chat rooms |
mod_http_upload | Sending photos and files (stored on your server’s disk) |
mod_http_upload_quota | Deletes uploaded files after max_days, so the disk does not fill up |
mod_shared_roster | Puts users into each other's contact lists, so the app can show who is online (Step 8). The short cache time makes changes show up at once. |
mod_muc_admin | Room commands such as create_room (on in the default config) |
mod_vcard, mod_blocking, mod_offline, mod_ping | Profile photos, blocking users, messages for people who are offline, and dropping dead connections (all on in the default config) |
If your app does not send photos or files, leave mod_http_upload out. Your disk stays clean and the app does not need storage permissions on Google Play.
Step 6: Start ejabberd and create the admin account
sudo systemctl restart ejabberd
sudo ejabberdctl status
sudo ejabberdctl register admin chat.yourdomain.com 'a-strong-password'
Open the web admin at https://chat.yourdomain.com:5443/admin and log in as admin@chat.yourdomain.com. From there you can see users, rooms and who is online.
If ejabberd does not start, the reason is in the log, almost always a YAML indentation mistake or a wrong database password:
sudo tail -n 50 /opt/ejabberd/logs/ejabberd.log
Step 7: Add the bad-word filter and auto-join rooms
Two more modules come from ejabberd-contrib, ProcessOne’s collection of extra modules:
| Module | What it does |
|---|---|
mod_pottymouth | Checks every chat and group message against your word list and replaces each blocked word with ****. It works on the server, so it filters messages from every app version. |
mod_default_rooms | When a new account is created, it adds your group rooms to the user’s bookmarks with auto-join on, so the user is in those rooms right after sign-up. |
A filter like this also helps with Google Play’s rules for apps with user-generated content, which expect you to moderate what people send.
Install the two modules
The ejabberd package can download and compile contrib modules by itself; it only needs git. ejabberd must be running:
sudo apt install -y git
sudo ejabberdctl modules_update_specs
sudo ejabberdctl module_install mod_pottymouth
sudo ejabberdctl module_install mod_default_rooms
sudo ejabberdctl modules_installed
The modules go into /opt/ejabberd/.ejabberd-modules. Each one has its own settings file in its conf folder, which ejabberd reads when it starts. You set them up below.
Set up the bad-word filter (mod_pottymouth)
1. The word list. One word per line, in lowercase:
sudo mkdir -p /opt/blacklist
sudo nano /opt/blacklist/blacklist_en.txt
2. The character map. It turns capitals and look-alike symbols into plain letters before checking, so B@D and b4d are caught by bad without listing every spelling. Save this as /opt/blacklist/charmap_en.txt, adding a line for every letter you need, and keep the final ].:
[
{"A", "a"},
{"B", "b"},
{"E", "e"},
{"@", "a"},
{"4", "a"},
{"3", "e"},
{"1", "i"},
{"0", "o"},
{"$", "s"}
].
3. Let ejabberd read the files, then point the module at them:
sudo chown -R ejabberd:ejabberd /opt/blacklist
sudo nano /opt/ejabberd/.ejabberd-modules/mod_pottymouth/conf/mod_pottymouth.yml
modules:
mod_pottymouth:
blacklists:
default: /opt/blacklist/blacklist_en.txt
en: /opt/blacklist/blacklist_en.txt
charmaps:
default: /opt/blacklist/charmap_en.txt
en: /opt/blacklist/charmap_en.txt
The module picks the list by the language tag on each message. Many apps tag messages as en, and messages with no tag use default, so give both the same list. For another language, add a line such as fr: with its own file.
Set up auto-join rooms (mod_default_rooms)
1. Create the rooms. They are permanent because of persistent: true in Step 5. You can also create them in the web admin:
sudo ejabberdctl create_room public conference.chat.yourdomain.com chat.yourdomain.com
sudo ejabberdctl create_room news conference.chat.yourdomain.com chat.yourdomain.com
2. List them in the module’s settings:
sudo nano /opt/ejabberd/.ejabberd-modules/mod_default_rooms/conf/mod_default_rooms.yml
modules:
mod_default_rooms:
rooms:
- "public@conference.chat.yourdomain.com"
- "news@conference.chat.yourdomain.com"
auto_join: true
3. Restart ejabberd so both modules load their settings:
sudo systemctl restart ejabberd
- The rooms are added when an account is created (from the app or with
ejabberdctl register). Accounts that existed before are not changed. - The app must support room bookmarks with auto-join. Apps based on XMPP standards, such as Conversations, do.
ejabberdctl module_upgradeoverwrites the files inconf. Keep a copy, or move the settings intoejabberd.yml.
Step 8: Show all users or online users in every contact list
A chat app needs a list of people to talk to. A shared roster group puts users into everyone’s contact list (the roster) automatically, with their online status, so nobody has to add contacts by hand. Apps that show “who is online” read this list. It comes from mod_shared_roster (Step 5).
| Show | Setting | Good for |
|---|---|---|
| All registered users | @all@ | Small and medium apps: everyone is in the list, and the app can show who is online |
| Only users who are online now | @online@ | Bigger apps: the list stays short, and people leave it when they go offline |
With @all@, every login receives the status of every user. With thousands of users that makes logins slower and uses more data, so move to @online@ as your app grows.
In the web admin
- Open
https://chat.yourdomain.com:5443/adminand log in asadmin@chat.yourdomain.com. - Go to Virtual Hosts → chat.yourdomain.com → Shared Roster Groups.
- Add a group named
everyone, with the labelEveryone(the label is the group name people see in the contact list). - Open the
everyonegroup. Set @all@ totrueto show all users, or set @online@ totrueto show only online users. - Under displayed groups, add
everyone. This step is easy to miss: it tells ejabberd to show the group to its own members. Without it, nobody sees anyone.
Or with ejabberdctl
The same group from the command line:
sudo ejabberdctl srg_add everyone chat.yourdomain.com
sudo ejabberdctl srg_set_info everyone chat.yourdomain.com label Everyone
sudo ejabberdctl srg_set_info everyone chat.yourdomain.com all_users true
sudo ejabberdctl srg_add_displayed everyone chat.yourdomain.com everyone
sudo ejabberdctl srg_get_info everyone chat.yourdomain.com
For online users only, use online_users true instead of all_users true. To switch later, set the one you no longer want to false. Users get the list the next time they log in.
Step 9: Install the coturn TURN server
sudo apt install -y coturn
sudo sed -i 's/^#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
sudo mv /etc/turnserver.conf /etc/turnserver.conf.original
sudo nano /etc/turnserver.conf
On Ubuntu, coturn will not start until TURNSERVER_ENABLED=1 is set in /etc/default/coturn; the sed line above does that. Put this in the new /etc/turnserver.conf:
listening-port=3478
listening-ip=0.0.0.0
external-ip=203.0.113.10
realm=chat.yourdomain.com
server-name=chat.yourdomain.com
fingerprint
lt-cred-mech
user=turnuser:a-long-random-password
# ports used to relay calls (open them as UDP in the firewall)
min-port=10000
max-port=20000
# security: no admin console, never relay into private networks
no-cli
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
syslog
user=is the TURN username and password. An app with a built-in TURN login must use exactly these two.external-ipis your public IP. It matters most on cloud servers where the network card has a private address.- The
denied-peer-iplines stop anyone from using your TURN server to reach private networks.
Start it and check that it listens on 3478:
sudo systemctl enable coturn
sudo systemctl restart coturn
sudo ss -lunp | grep 3478
Step 10: Open the firewall
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 5222/tcp
sudo ufw allow 5443/tcp
sudo ufw allow 3478
sudo ufw allow 10000:20000/udp
sudo ufw enable
sudo ufw status
| Port | What breaks without it |
|---|---|
| 5222 TCP | No app can log in |
| 5443 TCP | Web admin and photo or file sending |
| 80 TCP | Certificate renewal |
| 3478 UDP/TCP | Calls in apps that use coturn |
| 10000–20000 UDP | Calls ring but there is no sound or video |
Many VPS companies also have a firewall in their control panel (security groups on AWS, Oracle and Google Cloud). Open the same ports there, or the server firewall alone will not help.
Step 11: Test everything
- Chat: install any XMPP app (Conversations or Gajim, for example) and log in as
admin@chat.yourdomain.com. If it connects without a certificate warning, ejabberd and SSL are working. - TURN: open the WebRTC Trickle ICE test page. Add
turn:203.0.113.10:3478with your coturn username and password, and click Gather candidates. A line of typerelaymeans coturn works. - Calls: make a video call between two phones, both on mobile data with Wi-Fi off. That is the case TURN exists for.
- Bad-word filter: send a message with a word from your list between two accounts. It should arrive as
****. - Auto-join rooms: create a new account from the app. It should already be in your rooms.
- Users list: log in with two accounts. Each should see the other in the contact list, under your group's label.
Useful commands while testing:
sudo ejabberdctl connected_users
sudo ejabberdctl registered_users chat.yourdomain.com
sudo journalctl -u coturn -n 50
Common problems
| Problem | Usual cause |
|---|---|
| The app cannot connect at all | Port 5222 closed, or the domain in the app is not the one in hosts: |
| Certificate error in the app | Wrong path in certfiles, or the files are not owned by the ejabberd user |
| ejabberd stops right after starting | A tab or wrong indentation in ejabberd.yml, or a wrong MySQL password |
| Calls work on Wi-Fi but not on mobile data | TURN not reachable: check the ports, external-ip and turn_ipv4_address |
| Calls ring but there is no sound or video | The UDP relay port ranges are closed in a firewall |
| coturn is not listening on 3478 | TURNSERVER_ENABLED=1 is missing in /etc/default/coturn |
| People cannot sign up from the app | mod_register is missing or its ip_access blocks them |
module_install fails | git is not installed, or ejabberd is not running |
| Bad words are not filtered | Wrong path to the word list, the file is not readable by the ejabberd user, or the app tags messages with a language that has no list (add it, for example en:) |
| The app shows no users | The shared roster group has no displayed group, neither @all@ nor @online@ is set, or mod_shared_roster is missing. Log in again after fixing it. |
| New users are not in the rooms | The rooms do not exist, a room address is wrong, or the account was created before the module was set up |
Using it with our chat app source codes
Our Android live video chat app, video dating app and random video call app run on exactly this setup. Each purchase includes a ready server folder: the ejabberd 24.12 package, an ejabberd.yml already set up for the app, a step-by-step guide that includes coturn, mod_pottymouth and mod_default_rooms already compiled with a word list, and a fresh database with the app’s default rooms and a shared roster group that already shows users to each other. You replace the domain, IP and passwords, and the app connects.
Where to go next
- Build an Android chat app that connects to this server
- More about SSL certificates on Ubuntu
- Openfire, another XMPP server, if you want to compare