VPS & Servers

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.

A chat server connecting two phones on a video call through a TURN relay

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

PartWhat it does
ejabberd 24.12The chat server: accounts, messages, rooms, call signalling
MySQLStores users, contacts, rooms and offline messages
Let’s Encrypt certificateEncrypted connections; apps refuse servers without a valid one
coturn (port 3478)The TURN server: relays the voice and video of calls
mod_pottymouthBad-word filter: blocked words in messages arrive as ****
mod_default_roomsNew users join your group rooms automatically after sign-up
Shared roster groupAll 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_schema became sql_schema_multihost in 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
ModuleWhy the app needs it
mod_registerPeople can create an account from inside the app
mod_stun_discoTells apps about the TURN listener and gives them a short-lived login for it
mod_mucGroup chat rooms
mod_http_uploadSending photos and files (stored on your server’s disk)
mod_http_upload_quotaDeletes uploaded files after max_days, so the disk does not fill up
mod_shared_rosterPuts 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_adminRoom commands such as create_room (on in the default config)
mod_vcard, mod_blocking, mod_offline, mod_pingProfile 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:

ModuleWhat it does
mod_pottymouthChecks 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_roomsWhen 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_upgrade overwrites the files in conf. Keep a copy, or move the settings into ejabberd.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).

ShowSettingGood 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

  1. Open https://chat.yourdomain.com:5443/admin and log in as admin@chat.yourdomain.com.
  2. Go to Virtual Hosts → chat.yourdomain.com → Shared Roster Groups.
  3. Add a group named everyone, with the label Everyone (the label is the group name people see in the contact list).
  4. Open the everyone group. Set @all@ to true to show all users, or set @online@ to true to show only online users.
  5. 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-ip is your public IP. It matters most on cloud servers where the network card has a private address.
  • The denied-peer-ip lines 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
PortWhat breaks without it
5222 TCPNo app can log in
5443 TCPWeb admin and photo or file sending
80 TCPCertificate renewal
3478 UDP/TCPCalls in apps that use coturn
10000–20000 UDPCalls 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

  1. 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.
  2. TURN: open the WebRTC Trickle ICE test page. Add turn:203.0.113.10:3478 with your coturn username and password, and click Gather candidates. A line of type relay means coturn works.
  3. Calls: make a video call between two phones, both on mobile data with Wi-Fi off. That is the case TURN exists for.
  4. Bad-word filter: send a message with a word from your list between two accounts. It should arrive as ****.
  5. Auto-join rooms: create a new account from the app. It should already be in your rooms.
  6. 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

ProblemUsual cause
The app cannot connect at allPort 5222 closed, or the domain in the app is not the one in hosts:
Certificate error in the appWrong path in certfiles, or the files are not owned by the ejabberd user
ejabberd stops right after startingA tab or wrong indentation in ejabberd.yml, or a wrong MySQL password
Calls work on Wi-Fi but not on mobile dataTURN not reachable: check the ports, external-ip and turn_ipv4_address
Calls ring but there is no sound or videoThe UDP relay port ranges are closed in a firewall
coturn is not listening on 3478TURNSERVER_ENABLED=1 is missing in /etc/default/coturn
People cannot sign up from the appmod_register is missing or its ip_access blocks them
module_install failsgit is not installed, or ejabberd is not running
Bad words are not filteredWrong 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 usersThe 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 roomsThe 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

Questions people ask

Which ejabberd version should I install?
Install the version your app and configuration were built for. This guide and our chat app source codes use ejabberd 24.12, installed from the official ejabberd_24.12-1_amd64.deb package. Newer versions renamed some options, so a config written for 24.12 can stop a newer version from starting.
Why not install ejabberd with apt install ejabberd?
Ubuntu's own package is a different ejabberd version with different file paths and settings. The official 24.12 package from ProcessOne keeps everything in /opt/ejabberd, contains its own Erlang and matches the configuration in this guide.
Do I need a TURN server for video calls?
Yes, for calls to work reliably. When both phones are on Wi-Fi they can often connect directly, but on mobile data most networks block that, and the call needs a TURN server to relay the audio and video. Without one, calls ring and then show no picture or sound.
What is the difference between ejabberd's TURN and coturn?
Both relay calls. ejabberd's built-in TURN is announced to apps by mod_stun_disco with a short-lived login, which suits apps based on XMPP standards. coturn is a separate TURN server with a fixed username and password, which suits apps that have the TURN login written into them.
Which ports must be open for ejabberd and coturn?
5222 TCP for app logins, 5443 TCP for the web admin and file uploads, 80 TCP for certificate renewal, 3478 UDP and TCP for TURN, and the UDP relay range 10000-20000. Open them in ufw and in your VPS company's firewall.
How much RAM does ejabberd need?
ejabberd is light: 1 GB of RAM is enough for testing, and 2 GB is comfortable for a live app with MySQL and coturn on the same server. For calls, bandwidth matters more than RAM, because relayed video uses your server's traffic.
Do I have to import the database tables?
No. Since version 24.06, ejabberd creates and updates its MySQL tables by itself on the first start. You only create an empty database and a user for it.
Why do calls work on Wi-Fi but not on mobile data?
On mobile data the call has to go through TURN, so the TURN server is not reachable or not set up right. Check that the TURN ports and the UDP relay ranges are open, and that external-ip in coturn and turn_ipv4_address in ejabberd are your server's public IP.
How do I renew the SSL certificate for ejabberd?
Certbot renews Let's Encrypt certificates by itself. Add a deploy hook that copies the new fullchain.pem and privkey.pem to /opt/certs/xmpp, gives them to the ejabberd user and runs ejabberdctl reload_config, as shown in Step 4.
How do I block bad words in ejabberd chat?
Install mod_pottymouth from ejabberd-contrib with ejabberdctl module_install mod_pottymouth, give it a word list with one word per line and a character map for look-alike symbols, and restart ejabberd. Blocked words in chat and group messages then arrive as ****. It works on the server, so it filters messages from every app version.
How do I show all users or online users in the ejabberd roster?
Create a shared roster group with mod_shared_roster, in the web admin under Virtual Hosts, your domain, Shared Roster Groups, or with ejabberdctl srg_add. Set @all@ to true for all registered users or @online@ for online users only, and add the group to its own displayed groups. Users see the list the next time they log in.
How do new users join group rooms automatically in ejabberd?
Install mod_default_rooms from ejabberd-contrib, list your rooms in its settings and keep auto_join on. Every account created after that gets those rooms as bookmarks with auto-join, so the user is in the rooms right after sign-up. Accounts that existed before are not changed.
Can I stop users from filling my server with photos and files?
Yes. Leave mod_http_upload out if your app does not need file sending, or keep it and add mod_http_upload_quota with max_days so old uploads are deleted automatically.

Written by Habib Baloch

I build Android apps, websites and Ubuntu servers, and write down exactly how I did it.

Ask me about this guide →