Setting up Matrix with 1:1 audio/video calling and no federation

Hey everyone, this is my first tutorial so bear with me and ask questions! After the great tutorial on getting started with the Matrix client, a lot of people asked about how to get started with Matrix itself.

This tutorial takes us through my goal: my own Matrix server, that does not federate, that can do 1:1 audio/video calling, all with encryption. I did not worry with a Jitsi server because I don’t need group chat. So I know that this isn’t for everyone, but wanted to show what I had done. I really wanted to replicate the XMPP server I already had; the iPhone XMPP clients are unreliable, but Element works great on both Andriod and iOS!

After failing with a CentOS 8 droplet on this guide, I switched and used this guide as the principle parts for everything written below, so make sure to read through it too!

To start way back at step 1, the first thing you need is a domain from a registrar. Go ahead and get one if you don’t have one. The next thing is a Virtual Private Server (VPS) like Digital Ocean and point your domain to DigitalOcean’s nameservers.

The first thing I did the day before starting this, was to setup an A record. I used matrix.domain.tld as my Matrix domain throughout this tutorial. So keep an eye out if following the HowToForge, it mixes between calling it and!

So now that you have a domain and a subdomain, login to Digital Ocean and get yourself a droplet. I used Ubuntu 20.04 with the $5/mo droplet. Pick your datacenter, and click Create Droplet. At a minimum, follow this DO guide on the initial server setup.

Now that you have SSH working and you’re not logging in as root, let’s get started! Go ahead and update the base install. This gets everyhing up to date, and installs the needed Python3 packages.

sudo apt update && sudo apt upgrade

The next step is to add the matrix packages:

sudo apt install -y lsb-release wget apt-transport-https

sudo wget -qO /usr/share/keyrings/matrix-org-archive-keyring.gpg

Followed by

echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/matrix-org.list

Now that those are added, let’s install them!

sudo apt update
sudo apt install matrix-synapse-py3

Be sure to fill in the domain name correctly as matrix.domain.tld and selecting whether you want to send statistics or not.

From there, make sure it installed correctly and that you can start it
sudo systemctl start matrix-synapse

and then enable it to turn on at boot
sudo systemctl enable matrix-synapse

Make sure it’s up and running ok. If it’s active, you’re good to go!
systemctl status matrix-synapse

You should now make changes to the Synapse homeserver.yaml file. This is the main file that controls what happens with your Matrix instance. I will use nano for editing files in this tutorial. (Once you make the edit, hold ctrl+x to exit, if you want to save press Y, then press Enter to confirm the filename)

The main Matrix information is in /etc/matrix-synapse/ Before running the below, you can look at the whole file progressively with cat /etc/matrix-synapse/homeserver.yaml | less

Enter that directory by
cd /etc/matrix-syanpse
sudo nano homeserver.yaml

In nano, you can look for something with ctrl + w. That will help you jump around the yaml file without having to scroll a bunch!

Look for the listeners section and bind only to the local address


  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    bind_addresses: ['']

      - names: [client, federation]
        compress: false

The next step allows other to register themselves. I turned this to TRUE for my instance so I could let friends and family sign themselves up and create their own usernames. If you have other plans, think about whether you want people to register themselves or not. Please check the registration_shared_secret section in the HowToForge article.

enable_registration: true

I also chose to allow my server to transfer files up to 30M instead of the default 10M, so find max_upload_size and uncomment it and change to whatever file size you want.

Next step is generating SSL via LetsEncrypt. First, install LetsEncrypt
sudo apt install certbot

Now, create the SSL cert. Change your email from email@domain.tld and the actual domain from matrix.subdomain.tld

sudo certbot certonly --rsa-key-size 2048 --standalone --agree-tos --no-eff-email --email email@domain.tld -d matrix.domain.tld

Make sure everything populated by looking for the fullhain and privkey .pem files in

sudo ls -lah /etc/letsencrypt/live/matrix.domain.tld

Now that we have SSL enabled, let’s set up nginx to run as the reverse proxy. The HowToForge guide says it will serve 3 ports, but since I turned off federation, I don’t have it serving port 8448, which is the port used to talk to other Matrix instances.

sudo apt install nginx

It will install, and you should be able to navigate to

cd /etc/nginx/sites-available

We’ll create a new virtualhost that nginx will use, and we’ll call it ‘matrix’

sudo nano matrix

Below is the config file to use. It routes regular HTTP traffic to the HTTPS port and tells the HTTPS port what you want to do with that traffic. Again, if you want federation, refer to the HowToForge document for the port 8448 documentation. Make sure to increase the nginx traffic to match the upload size in homeserver.yaml if you changed that above.

server {
    listen 80;
    server_name matrix.domain.tld;
    return 301 https://$host$request_uri;

server {
    listen 443 ssl;
    server_name matrix.domain.tld;

    ssl_certificate /etc/letsencrypt/live/matrix.domain.tld/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.domain.tld/privkey.pem;

    location /_matrix {
        proxy_pass http://localhost:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        # Nginx by default only allows file uploads up to 1M in size
        # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
        client_max_body_size 30M;

Save and close that config. Then we’ll activate that configuration and have nginx check it for errors. I had mistyped a line the first time and this will pop up and tell you what line it has a hard time reading; make sure to follow the open and close brackets, I had added one too many originally.

sudo ln -s /etc/nginx/sites-available/matrix /etc/nginx/sites-enabled/

to turn it on and

sudo nginx -t

to test. Once it passes, restart nginx to use the config with

sudo systemctl restart nginx

and then turn on nginx in case of a reboot with

sudo systemctl enable nginx

Now that nginx is up, you need to open the firewall ports to allow traffic to it! Again, I did not open port 8448 on the firewall, to prevent federation. If you want to federate, you would want to allow that port here.

sudo allow https
sudo allow ssh
sudo ufw enable

Then check your ports with sudo ufw status numbered. It should only be showing ports you’ve allowed open so far.

OK, the server is up and ready. Now to get started with Matrix. Create a user, which we’ll make an admin once we get to the config.

sudo register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://localhost:8008

When it asks if you want to make an admin, choose yes.

Now, you should be able to login using the Element mobile app, choose your server, and login as this admin user! If it can’t find your server, check your nginx configuration and make sure you can ping your subdomain with

ping matrix.domain.tld

With the present setup however, you can only text people at this point. Let’s add the ability to call them! To do this, we’ll setup a TURN server. I am pulling most of the TURN information from this link.

sudo apt-get update && sudo apt-get install coturn

Make a backup of the initial config in case you break something!
sudo mv /etc/turnserver.conf /etc/turnserver.conf.back

Now, I’ve used the same TURN config that they provide, so make sure you download a copy to your home directory

cd ~ wget -O turnserver.conf

Then we’ll edit it. This part is the important piece, because you’ll need to keep track of some things for the homeserverl.yaml. Let’s look at the initial config using cat turnserver.conf so we can see what we’ll be editing first.

Firstly, I have left open all the UDP ports. I intially left a smaller range open, but I couldn’t get the calling to work. So in this tutorial, we’ll leave the listening port and tls-listening port alone, and leave the UDP range as minimum of 49152 and the max as 65535. In the config file where it says listening-ip you will want to make it be the public IP of your droplet that we’ve been working on. This can be pulled from Digital Ocean or is likely already in your bash from connecting to it via SSH. The realm portion will need to be matrix.domain.tld that we’ve been using. We will also need to change the Let’s Encrypt certificate paths to match what we just did:

cert=/etc/letsencrypt/live/matrix.domain.tld/cert.pem and pkey=/etc/letsencrypt/live/matrix.domain.tld/privkey.pem

The other main ingredient here is the static-auth-secret. To replace this, you should open another shell on your local computer, and enter openssl rand -base64 30. Copy this somewhere, because we’ll need it again in a second.

Using nano turnserver.conf, make the changes we talked about, and paste in the static-auth-secret that you just generated. Save the file and close it. From here, we’ll copy the configurtion file to its new home with

sudo cp turnserver.conf /etc/turnserver.conf

Now we’ll uncomment TURNSERVER_ENABLED=1 in the coturn file with

sudo nano /etc/default/coturn

Great, the TURN server should be ready to go. But what about Matrix? Let’s take that static-auth-secret that we copied earlier and put it in our homeserver.yaml file so that Matrix knows we want to use TURN and can talk to the coturn module. We’ll turn on the TURN module, allow guests to use it, and put in the TURN secret. You can read the official Matrix documentation on it here

sudo nano /etc/matrix-synapse/homeserver.yaml

Below is the whole block of code for the TURN section. Change the turn_uris to match your domain, and udate the turn_shared_secret to be the secret generated from the turnserver.conf file. Remember you can use ctrl + w to search for ## TURN## instead of scrolling a lot!

## TURN ## 

# The public URIs of the TURN server to give to clients 
turn_uris: [ "turn:matrix.domain.tld:3478?transport=udp", "turn:matrix.domain.tld:3478?transport=tcp" ] 

# The shared secret used to compute passwords for the TURN server 
turn_shared_secret: "EnterYourSecretHereBetweenTheQuotes" 

# The Username and password if the TURN server needs them and 
# does not use a token 
#turn_username: "TURNSERVER_USERNAME" 
#turn_password: "TURNSERVER_PASSWORD" 

# How long generated TURN credentials last 
turn_user_lifetime: 86400000 

# Whether guests should be allowed to use the TURN server. 
# This defaults to True, otherwise VoIP will be unreliable for guests. 
# However, it does introduce a slight security risk as it allows users to 
# connect to arbitrary endpoints without having first signed up for a 
# valid account (e.g. by passing a CAPTCHA). 
turn_allow_guests: True

OK, almost done! Your Matrix server now knows you want to use TURN, but we haven’t opened the correct ports on the firewall for the base server to let traffic through. Remember the HTTP and HTTPS ports from the turnserver.conf? That’s how we got the ports for the turn_uris in homeserver.yaml. So we need to open those ports, plus the UDP range of ports from the turnserver.conf as well.

There are faster ways to do this, but this shows us allowing the type of traffic per port.

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
sudo ufw allow 5349/udp
sudo ufw allow 49152:65535/udp

At this point, everything should be up! You have a server, with Matrix running, encrypted via Let’s Encrypt, with nginx doing a reverse proxy, and a TURN server to setup 1:1 audio/video calling. Just restart all the services, get someone to join your instance and try calling them! [On an unrelated note, try to join them from not the same network. On XMPP, I could connect from my home network fine but could not call from a cell data, as I had a misconfiguration]

sudo service ufw restart sudo systemctl restart coturn sudo systemctl restart matrix-synapse

That’s the end! You should be able to communicate with any friends and family you can convince to join your network! I’ve been able to hold video calls locally and globally to catch up with friends, and the quality has been really great.

Some things to mention:

  1. I am not a security expert, but I believe this configuration to be good enough. Please comment if you see any obvious, glaring holes.
  2. I need to work on a way to automate this as well, but writing the guide should help me be able to duplicate my steps if I were to need to set it up again (or for someone else!).
  3. Organization is hard! Keeping track of the SSH keys, the passwords, the IPs, the secrets, etc…maybe I should invest in Bitwarden!
  4. If you have a base domain and nothing else on there this Docker and Ansible guide is probably WAY better than what I have. It takes a lot of reading through, but it should setup all of this for you with just a few clicks, once you change some of the configurations. However, setting it up on a matrix.domain.tld subdomain broke the Let’s Encrypt portion and I just wanted Matrix up, not to go buy another subdomain.
  5. Getting people to join has been OK. Mostly they just tell me their username and I can create the 1:1 room. I’ve only had 1 instance where it wasn’t encrypted by default but we did the ‘verify’ token and it’s encrypted now.

Wow. Thank you for sharing Snorlax!

Another big WOW! Thank you!
Can I suggest that you need this too.

Start coturn and enable it to start on boot
$ sudo systemctl enable --now coturn

Sorry If I missed it in your fantastic post.
I also managed to have a three way video conference using the info from your post to setup.

Thanks again Snorlax.


Thanks. It seems to be working fine in any case, without that. Because I’ve rebooted the server and it comes back up. I’ll look at it though.

Glad this helped. How did you get 3-way video to work? I haven’t worried about it because 1:1 has been fine, but I am at the point where I was going to look up the Jitsi add-on.

I use the browser based version of Element. Created a public room within my unfederated instance and that’s about it. When participants entered the room and enabled video chat it all just worked. I did notice that Jitsi automagically came into play as well.

I wonder if it’s just a Matrix update. I think Noah mentioned on a recent show they were trying to have conferencing ‘just work’. I’ll have to give it a go.