Setting up Matrix Synapse + Element Call from scratch with podman

Table of Contents

It’s been a while since I first setup a Matrix Synapse server. A few things changed since then: installing via container is now the way to go and also Matrix/Element introduced a new way to make audio and video calls not relying on Jitsi anymore. Making all of this work is a little tricky and most of the instructions I found were related to Docker - but I wanted to install via Podman, which I think is the better choice nowadays. Of course, there a similarities but the little differences do count… So, in case you’d like to set up your own Matrix based chat server, I hope the following will be of help.

I will not go into details about what Matrix/Element is or how to set up a Linux server - if you’ve found your way here, you’d probably know that kind of stuff.

In the end you should have:

  • Synapse server running connected to a Postgresql instance
  • Matrix Authentication Service (MAS), also connected to a Postgresql instance
  • Element Web
  • Element Call
  • we will use delegation for the synapse server as it will be available at matrix.example.com whereas the user names will be @<user>:example.com

Prerequisites

Server (VPS)

In order to start you will of course need a server. I guess any VPS will do, probably 1 CPU and 2 GB of RAM are fine for very small deployments, but with 2 CPUs and 4 GB you’re on the safe side. Also note, that depending on federation and the rooms you’ll join, it will also require some amount of disk space. Having around ~100 GB available as a start you should be fine and still have some buffer. Install any recent Linux distribution, but make sure it provides not too outdated versions of Podman. Any recent Ubuntu LTS release is probably a good choice. Restrict logins as good as possible and also install a firewall (in my example I will use ufw) and fail2ban for hardening purposes.

DNS records

You will also need a domain. Register one wherever you like, for the ease of this documentation I will go with “example.com”. Go to your provider’s DNS settings and create A and AAAA records for example.com pointing to your server’s IPs. Then create some additional CNAMEs, i.e. matrix.example.com, mrtc.example.com, web.example.com and mas.example.com, all pointing to example.com.

  • example.com: this will be the last part of your usernames, i.e.: @USERNAME:example.com
  • matrix.example.com: this is where synapse will be listening to
  • mrtx.example.com: required for Element Call
  • web.example.com: Element web client
  • mas.exmaple.com: Matrix authentication service

Prepare the server

I will assume the server is running Ubuntu or Debian here. If you’ve chosen a different distro, commands will vary. If not indicated otherwise, run commands as root.

Additional packages

First install some packages we will require later:

apt install podman nginx

nginx will be our reverse proxy and basically the only application required for our setup not running via podman.

Certificates

We will use Certbot to get our certificates, so checkout it’s documentation. For Ubuntu and alike:

snap install --classic certbot
ufw allow 80/tcp
ufw allow 443/tcp
certbot certonly --nginx  # create certificates for "example.com", "matrix.example.com", "web.example.com", "mas.example.com" and "mrtc.example.com"

Application user

The actual application will be running as unprivileged user, so go ahead and create one. There are different ways to do this, i.e.:

groupadd matrix
useradd -m -d /home/matrix -s /usr/bin/bash -c "Matrix User" -g matrix matrix

The user should not have a password and should be locked! (Check /etc/shadow there should be one - or two - ! at the second column.)

Switch to the new user to see if all is fine, exit again and then run loginctl to make sure our services will be running later even if the user is not logged in:

su - matrix
exit
loginctl enable-linger matrix

For later add the $XDG_RUNTIME_DIR to the user’s .bashrc:

echo "export XDG_RUNTIME_DIR=/run/user/$(id -u matrix)" >> /home/matrix/.bashrc

Software installation

From here on we will use the application user we just created. I will stick to the example user matrix here. So, switch to it now.

As mentioned, we will use Podman to get everything up and running making use of its quadlet feature generating systemd unit files (s. here).

All the containers will require some persistent storage or configuration files. For this purpose create a directory within the home of the application user, i.e.:

mkdir ~/application-stack

Postgresql

Though Postgresql is technically not required for Synapse (for MAS it is!), it’s recommended over SQlite. Most of the examples I found were using version 14, but I’d go with the latest one (as the time of writing it’s version 18). So, we’ll spin up a container once and initialize the required DB files. This requires setting a password. You will most likely not need it in the future, but in any case, note it down somewhere and don’t make it an easy one. We’ll use the application-stack directory to create a subdirectory for our Postgresql database files.

mkdir -m 755 ~/application-stack/postgresql
podman run --rm --name postgres -v /home/matrix/application-stack/postgresql:/var/lib/postgresql -e POSTGRES_PASSWORD="<some_password>" -d docker.io/postgres:18-alpine

This should have created the new directory 18 within ~/application-stack/postgresql and you should see the container is up by executing:

podman ps

While the container is still up, we’ll connect to it and create two users and two databases, one for the synapse server and one for the Matrix authentication service (again: note down the passwords you choose):

podman exec -it postgres psql -U postgres

While in the psql prompt:

CREATE USER matrixdbuser WITH PASSWORD '<another_password_here>';
CREATE DATABASE matrixdb ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' template=template0 OWNER matrixdbuser;
CREATE USER masdbuser WITH PASSWORD '<yet_another_password_here>';
CREATE DATABASE masdb OWNER masdbuser;
\l
\q

\l will list the databases and with \q you’ll quit the psql prompt. Now we can stop the postgresql container again:

podman stop postgres

HINT: For some minimal security gain, you could replace trust by scram-sha-256 in ~/application-stack/postgresql/18/docker/pg_hba.conf (this will require root).

Synapse

Next we’ll prepare the synapse server. Let’s create a new directory to hold its persistent files and generate an initial configuration:

mkdir -m 700 ~/application-stack/synapse
mkdir -m 755 ~/application-stack/synapse/data
podman run -it --rm -v /home/matrix/application-stack/synapse/data:/data -e SYNAPSE_SERVER_NAME=example.com -e SYNAPSE_REPORT_STATS=no docker.io/matrixdotorg/synapse:latest generate

This will generate some config files in /home/matrix/application-stack/synapse/data and we need to edit homeserver.yaml:

  • server_name: should read example.com

  • change/add: public_baseurl: https://matrix.example.com/

  • remove the current database block and replace it by:

    database:
      name: psycopg2
      txn_limit: 10000
      args:
        user: matrixdbuser
        password: <PASSWORD_YOU'VE CHOSEN ABOVE FOR 'matrixdbuser'>
        database: matrixdb
        host: postgresql
        port: 5432
        cp_min: 5
        cp_max: 10
    

    HINT: the host part of the database configuration will simply be the hostname of our Postgresql container, in this example postgresql.

  • for Element call to work add:

    max_event_delay_duration: 24h
    experimental_features:
      msc3266_enabled: true
      msc4222_enabled: true
      msc4140_enabled: true
    rc_message:
      per_second: 0.5
      burst_count: 30
    rc_delayed_event_mgmt:
      per_second: 1
      burst_count: 20
    
  • for Matrix authentication service we’ll need to add:

    matrix_authentication_service:
      enabled: true
      endpoint: http://mas:9085/
      secret: "<something_secret>"
    

    HINT: mas will be the hostname of our Matrix authentication service container. “something_secret” will be generated later, don’t mind this right now.

As we will be using Matrix authentication service for the user management, I’m not sure if enable_registration: false will still work if you want to disable user registration. In my overall setup it’s still disabled. So, depending on your needs, add this or don’t add this to homeserver.yaml.

  • configure email
  • please s. here for all possible configuration options

At this point we already have a working Matrix server configuration - but without voice/video chat and missing a authentication. So, let’s get the additional components configured!

Livekit

Livekit will be responsible for the voice/video chats and we will need a basic configuration here as well. This time, we have to generate it ourselves. I found a few sources, some of them differed in details, but this is what I ended up with. First, we’ll generate a key + secret:

podman run --rm docker.io/livekit/livekit-server:latest generate-keys

It will return an “API Key” and an “API Secret”. Copy both somewhere, we’ll need them in a minute. Next, create a new directory for the configuration file:

mkdir -m 700 ~/application-stack/livekit

… and create ~/application-stack/livekit/livekit.yaml (replace <API Key>: <API Secret> with the actual values created before - the space between : and <API Secret> is crucial!):

port: 7880
bind_addresses:
  - ""
rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 50200
  use_external_ip: true
  enable_loopback_candidate: false
turn:
  enabled: false
  domain: mrtc.example.com
  cert_file: ""
  key_file: ""
  tls_port: 5349
  udp_port: 3478
  external_tls: true
keys:
  <API Key>: <API Secret>
logging:
  level: info

JWT

The JWT service will bring calls and a Matrix room together. It requires some variables, which you can either set in the quadlet file (s. below) or - and I’d prefer this - create a dedicated env file:

mkdir -m 700 ~/application-stack/jwt

Now create ~/application-stack/jwt/.env with the following content (you will need the API key and API Secret from before again):

LIVEKIT_URL=wss://mrtc.example.com/livekit/sfu
LIVEKIT_KEY=<API Key>
LIVEKIT_SECRET=<API Secret>
LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com

Element Web

Element Web is optional, but it provides a nice and easy way to access your chat server. As before, create a new directory and add a config file to it:

mkdir -m 700 ~/application-stack/element-web

~/application-stack/element-web/config.json:

{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.example.com",
            "server_name": "example.com"
        },
        "m.identity_server": {
            "base_url": "https://vector.im"
        }
    },
    "brand": "example.com Chat",
    "integrations_ui_url": "https://scalar.vector.im/",
    "integrations_rest_url": "https://scalar.vector.im/api",
    "integrations_widgets_urls": [
        "https://scalar.vector.im/_matrix/integrations/v1",
        "https://scalar.vector.im/api",
        "https://scalar-staging.vector.im/_matrix/integrations/v1",
        "https://scalar-staging.vector.im/api",
        "https://scalar-staging.riot.im/scalar/api"
    ],
    "default_theme": "dark",
    "room_directory": {
        "servers": ["matrix.example.com"]
    },
    "features": {
        "feature_video_rooms": true,
        "feature_group_calls": true,
        "feature_element_call_video_rooms": true
    },
    "setting_defaults": {
        "breadcrumbs": true
    }
}

If you don’t want integrations, you can of course remove those parts.

Matrix Authentication Service

User authentication and management will be done by Matrix’ authentication service, not by the synapse server itself anymore. For details see here. Again, create a dedicated directory and then start the container to generate a basic configuration file:

mkdir -m 700 ~/application-stack/mas
podman run ghcr.io/element-hq/matrix-authentication-service config generate > /home/matrix//application-stack/mas/config.yaml

Check the content of the file. Change the port to 9085, make sure to change public_base and issuer accordingly, adjust the database uri and the matrix configuration (pointing to your synapse server):

http:
  listeners:
  - name: web
    .
    .
    .  
    binds:
    - address: '[::]:9085'

  public_base: https://mas.example.com/
  issuer: https://mas.examnple.com/
.
.
.
database:
  uri: postgresql://masdbuser:<PASSWORD_YOU'VE CHOSEN ABOVE FOR 'masdbuser'>@postgresql/masdb
.
.
.
matrix:
  kind: synapse
  homeserver: example.com
  secret: "<something_secret>"
  endpoint: http://synapse:8008/

HINT: the host part of the database configuration will simply be the hostname of our Postgresql container, in this example postgresql. Same goes for the synapse container in the matrix part of the configuration which we’ll simply set to synapse.

Make sure to copy the secret “something_secret” to the matrix_authentication_service part of your homeserver.yaml now!

For a full overview of configuration options s. here. (Might be a good idea to configure email.)

We have now completed the initial configuration of all services. The magic happens by creating quadlets :-)

Quadlets

For each of our containers we will now create a quadlet, which will then generate a systemd unit file. If not already present, create some directories:

mkdir -p ~/.config/containers/systemd
chmod 700 .config

Networks

We will use two networks: one internal and one external. As Postgresql does not need to be available out side of our server, it will be in the internal network. The Synapse container and the MAS container will be part of the internal network (so they can connect to the Postgresql DB) and of the external network as they need to be available for our nginx reverse proxy. All other containers will be in the external network only. So, create those two network files:

~/.config/containers/systemd/int_network.network:

[Network]
Label=app=int_network
Internal=true
NetworkName=int_network

~/.config/containers/systemd/ext_network.network:

[Network]
Label=app=ext_network
NetworkName=ext_network

Postgreql quadlet

Create the quadlet for the postgresql container ~/.config/containers/systemd/postgresql.container:

[Unit]
Description=Postgresql Service
After=network-online.target

[Container]
Image=docker.io/postgres:18-alpine
ContainerName=matrix_db
AutoUpdate=registry
Volume=/home/matrix/application-stack/postgresql:/var/lib/postgresql:Z
HealthCmd=CMD pg_isready -U postgres
HealthInterval=10s
HealthRetries=5
HealthTimeout=15s
HostName=postgresql
Network=int_network.network
ShmSize=256mb

[Service]
Restart=always
TimeoutStartSec=600

[Install]
WantedBy=default.target

Make sure HostName corresponds to the database configuration parts for synapse and mas!

Synapse quadlet

The quadlet for the synapse container should look like this ~/.config/containers/systemd/synapse.container:

[Unit]
Description=Synapse
After=network-online.target
Requires=postgresql.service
Requires=mas.service

[Container]
Image=docker.io/matrixdotorg/synapse:latest
ContainerName=synapse
AutoUpdate=registry
Volume=/home/matrix/application-stack/synapse/data:/data:Z
HealthCmd=CMD-SHELL curl -s --noproxy localhost localhost:8008/health | grep -q 'OK' || exit 1
HealthInterval=10s
HealthRetries=5
HealthTimeout=15s
HostName=synapse
Network=ext_network.network
Network=int_network.network
PublishPort=127.0.0.1:8008:8008

[Service]
Restart=always
TimeoutStartSec=600

[Install]
WantedBy=default.target

Also here: make sure HostName matches the endpoint: configuration of Matrix authentication services’ config.yaml.

Livekit

Livekit’s quadlet ~/.config/containers/systemd/livekit.container:

[Unit]
Description=Livekit
After=network-online.target

[Container]
Image=docker.io/livekit/livekit-server:latest
ContainerName=livekit
AutoUpdate=registry
Exec=--config /etc/livekit.yaml
Volume=/home/matrix/application-stack/livekit/livekit.yaml:/etc/livekit.yaml:Z,ro
Network=ext_network.network
AddHost=example.com:host-gateway
AddHost=matrix.example.com:host-gateway
AddHost=mrtc.example.com:host-gateway
PublishPort=127.0.0.1:7880:7880
PublishPort=7881:7881
PublishPort=50000-50200:50000-50200/udp

[Service]
Restart=always
TimeoutStartSec=600

[Install]
WantedBy=default.target

The AddHosts are quite important. I was really struggling with getting Element Call to work and then thankfully found the solution at GNU/Linux.ch.

JWT quadlet

The JWT quadlet ~/.config/containers/systemd/jwt.container:

[Unit]
Description=Element Call
After=network-online.target
Requires=livekit.service

[Container]
Image=ghcr.io/element-hq/lk-jwt-service:latest
ContainerName=jwt
AutoUpdate=registry
EnvironmentFile=/home/matrix/application-stack/jwt/.env
AddHost=example.com:host-gateway
AddHost=matrix.example.com:host-gateway
AddHost=mrtc.example.com:host-gateway
Network=ext_network.network
PublishPort=127.0.0.1:8080:8080

[Service]
Restart=always
TimeoutStartSec=600

[Install]
WantedBy=default.target

Again: mind the AddHosts!

Element web quadlet

The Element web quadlet ~/.config/containers/systemd/element-web.container:

[Unit]
Description=Element Web
After=network-online.target

[Container]
Image=docker.io/vectorim/element-web:latest
ContainerName=element-web
AutoUpdate=registry
Volume=/home/matrix/application-stack/element-web/config.json:/app/config.json:Z,ro
Environment=ELEMENT_WEB_PORT=5080
Network=ext_network.network
PublishPort=127.0.0.1:5080:5080

[Service]
Restart=always
TimeoutStartSec=600

[Install]
WantedBy=default.target

Matrix authentication service quadlet

The MAS quadlet should look like this .config/containers/systemd/mas.container

[Unit]
Description=Matrix Auth
After=network-online.target
Requires=postgresql.service

[Container]
Image=ghcr.io/element-hq/matrix-authentication-service:latest
ContainerName=matrix-auth
AutoUpdate=registry
Environment=MAS_CONFIG=/app/config/config.yaml
Volume=/home/matrix/application-stack/mas/config.yaml:/app/config/config.yaml:Z,ro
HostName=mas
Network=ext_network.network
Network=int_network.network
PublishPort=127.0.0.1:9085:9085

[Service]
Restart=always
TimeoutStartSec=600

[Install]
WantedBy=default.target

Make sure HostName matches the endpoint of the matrix_authentication_service part of your homeserver.yaml.

HINT: If you dont want to autoupdate your container images, remove AutoUpdate=registryfrom you quadlets.

Starting the services for the first time

Now this is the moment of truth… Before we start all containers, we need to actually let systemd know about them (we’re still logged in as the application user matrix!):

systemctl --user daemon-reload

You can of course start all containers at once, but I’d recommend doing that one by one at this point. After starting one, wait a little and see if it stays up. The synapse and mas container might take a little as they will initially create some tables in there respective databases. In case of problems you can either check logs via journalctl --user -u <unit> or - if the container stays up - via podman logs -f <container_name>. See containers running by podman ps. So:

systemctl --user start postgresql
systemctl --user start mas
systemctl --user start synapse
systemctl --user start livekit
systemctl --user start jwt
systemctl --user start element-web

For MAS you can check the configuration syntax by:

podman exec -it matrix-auth mas-cli config check --config=/app/config/config.yaml

And you can also see if it’s working correctly:

podman exec -it matrix-auth mas-cli doctor

You can ignore errors about “.well-known” URIs at this point.

Auto updates

Optionally, you can enable automatic updates of all containers to which you’ve added AutoUpdate=registry in their respective quadlet file by enabling the auto-update service and timer:

systemctl --user enable podman-auto-update.service
systemctl --user enable podman-auto-update.timer

Nginx reverse proxy

If all containers are running fine, we can now configure nginx as one of our last steps. What do we need?

Containerlocal portpublic port(s)
synapse8008443 + 8448
web5080443
mas9085443
livekit7880443
jwt8080443

I’d recommend creating a dedicated configuration file in /etc/nginx/sites-available for each (sub)domain. I will outline the important parts only and leave out individual configuration, like path to certificates, ciphers, etc. I basically copied most of that from the official documentation. Of course, for this we need to switch to root again.

example.com

example.com does not really serve any part of the application. So, you could create some generic index.html file to serve. However, we will need to serve some .well-known URIs here:

server {
        listen 443 ssl;
        listen [::]:443 ssl;

        server_name example.com;

        root /var/www/html;
        index index.html

        .
        .
        .

        location / {
                # attempt to serve request as file, then as
                # directory and then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

        location /.well-known/matrix/client {
                return 200 '{"m.homeserver": {"base_url": "https://matrix.example.com"}, "org.matrix.msc3881": { "enabled": true, "url": "https://mrtc.matrix.example.com" }, "org.matrix.msc4143.rtc_foci": [ { "type": "livekit", "livekit_service_url": "https://mrtc.example.com/livekit/jwt" } ], "org.matrix.msc2965.authentication": { "issuer": "https://mas.example.com/", "account": "https://mas.example.com/account/" } }';
                default_type application/json;
                add_header Access-Control-Allow-Origin *;
        }

        location /.well-known/matrix/server {
                return 200 '{"m.server": "matrix.example.com"}';
                default_type application/json;
                add_header Access-Control-Allow-Origin *;
        }
}

matrix.example.com

This is where the synapse server will be reachable. Thus we need to listen to ports 443 and 8448 (federation) and forward that to 8008. Also, we need to forward requests to the authentication service here. Nevertheless, there’s nothing in “/”, thus we can again serve a generic html file here:

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    # For the federation port
    listen 8448 ssl default_server;
    listen [::]:8448 ssl default_server;

    server_name matrix.example.com;

    root /var/www/html;
    index index.html;
    
    .
    .
    .

    # Forward to the auth service
    location ~ ^/_matrix/client/(.*)/(login|logout|refresh) {
        proxy_http_version 1.1;
        proxy_pass http://localhost:9085;
        # OR via the Unix domain socket
        #proxy_pass http://unix:/var/run/mas.sock;

        # Forward the client IP address
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # or, using the PROXY protocol
        #proxy_protocol on;
    }

    # Forward to Synapse
    location ~ ^(/_matrix|/_synapse/client|/_synapse/mas) {
        # note: do not add a path (even a single /) after the port in `proxy_pass`,
        # otherwise nginx will canonicalise the URI and cause signature verification
        # errors.
        proxy_pass http://localhost:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;

        # 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 50M;

        # Synapse responses may be chunked, which is an HTTP/1.1 feature.
        proxy_http_version 1.1;
    }
}

mas.example.com

All requests for the authentication service need to be forwarded to port 9085:

server {
        listen 443 ssl;
        listen [::]:443 ssl;

        server_name mas.example.com;

        .
        .
        .

        location / {
                proxy_http_version 1.1;
                proxy_pass http://localhost:9085;
                # OR via the Unix domain socket
                #proxy_pass http://unix:/var/run/mas.sock;

                # Forward the client IP address
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                # or, using the PROXY protocol
                #proxy_protocol on;
        }
}

web.example.com

This one is also quite easy, we just need to forward all requests to our web chat to port 5080:

server {
        listen 443 ssl;
        listen [::]:443 ssl;

        server_name web.example.com;

        .
        .
        .

        # element-web
        location / {
                proxy_pass http://localhost:5080;
                proxy_set_header Host $host;
        }
}

mrtc.example.com

To make Element call work, we’ll need to forward requests to livekit and JWT. As there’s nothing for /, let’s point it to the static index.html file again:

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name mrtc.example.com;

    root /var/www/html;
    index index.html;

    .
    .
    .

     location ^~ /livekit/jwt/ {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_pass http://localhost:8080/;
    }

    location ^~ /livekit/sfu/ {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_send_timeout 120;
      proxy_read_timeout 120;
      proxy_buffering off;
      proxy_set_header Accept-Encoding gzip;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_pass http://localhost:7880/;
    }
}

Enable the new configuration

Create symlinks to each configuration file within /etc/nginx/sites-enabled and remove the symlink for default. It should look something like this:

user@server:~# ls -l /etc/nginx/sites-enabled/
total 0
lrwxrwxrwx 1 root root 42 Jun 12 15:27 mas.example.com -> /etc/nginx/sites-available/mas.example.com
lrwxrwxrwx 1 root root 44 Jun 12 15:27 matrix.example.com -> /etc/nginx/sites-available/matrix.example.com
lrwxrwxrwx 1 root root 37 Jun 12 15:27 example.com -> /etc/nginx/sites-available/example.com
lrwxrwxrwx 1 root root 41 Jun 12 15:27 mrtc.example.com -> /etc/nginx/sites-available/mrtc.example.com
lrwxrwxrwx 1 root root 42 Jun 12 15:27 web.example.com -> /etc/nginx/sites-available/web.example.com

Restart nginx:

systemctl restart nginx

If there are no syntax errors it should come up fine - fingers crossed…

Firewall

Lastly, we’ll need to open some additional firewall ports:

ufw allow 8448/tcp
ufw allow 7881/tcp
ufw allow 50000:50200/udp
  • 8448: Matrix federation
  • 7881: Livekit (as backup for UDP connections)
  • 50000:50200: Livekit

Adding a user

If you disabled user registration, you can add them manually by using mas-cli command within the mas container (as matrix user of course):

podman exec -it matrix-auth mas-cli manage register-user

See here for a list of possible manage commands.

Conclusion

At this point you should have your own E2EE chat server running, including E2EE (video-)calls. It took quite a while for me until everything was set up and you will probably - like me - stumble over a few syntax errors allong the way, but it’s managable. For me stuff like this is always kind of fun - even if it get’s a little frustratng somtimes if things don’t work as expected right away… 😉