Rapture in Everything

Setting up a Matrix chat server


Last time I started writing this post, I ended up explaining how federated services work in general, and what Matrix can do in particular. This post will describe how to set up a homeserver of your own and join the Matrix chat network.

(Update: The third part, Setting up Bridges, is here.)

Initial considerations

I will be using the reference server implementation, Synapse. It offers two options for data storage: Postgres or SQLite. Since I only plan to use it myself, and even then I'm not sure whether I'll actually end up using it much (any chat platform is only useful if people you want to talk to actually use it, however cool it might be) - I'll go with SQLite. If any other components offer similar options, I will always go with SQLite and avoid a dedicated DB server.

The entire setup will run in Docker containers. The official repos link to an Ansible deployment script that will set up everything you need with many bells and whistles, including an identity server, bridges and whatnot. I chose not to go this route, since I want to actually understand what components I'm running and what do they do. For me, part of the charm of self-hosting is I know what's done to my data and where it is - if I install a dozen components without having any idea what they individually do, a big part of that is lost.

I also prefer named volumes in Docker. Plenty services only include mapped folders in their install instructions, but I always changed them to named volumes.

I will be using docker-compose. I keep the docker-compose.yml file in a folder called matrix. That also means all names (of containers, volumes...) will have matrix_ prefix. I.e. if the dockerfile says synapse, the actual container will be called matrix_synapse_1. This is just a heads-up in case you're not familiar with docker-compose, I will not be mentioning it in the rest of the article so keep it in mind.

In theory, the only thing I actually have to run is the Matrix server, Synapse. I could then use a client on my phone, or even Element's web app (renamed from Riot, eh) to connect to my server and be done with it.

I, however, also want to host all the things I will need myself. So I will use Element web as my chat platform and configure it to connect to my homeserver by default. (Element also has apps for phones and desktop.)

When I finally managed to get it up and running, I found out there is no admin interface in Element, so I also set up Synapse Admin to be able to fix the mess I made of my users edit users and rooms.

The whole thing will run under Ubuntu 18 LTS, behind a nginx reverse proxy, with Let's Encrypt certificates.


When you first set up a Synapse server, you have to give it a name, which can not be changed later and it will show up in the user names of every user on this server. So if my server is on zble.sk, every username will be: @user:zble.sk.

But if for any reason the Synapse server can't actually run directly on the domain you wish to use, you can set up a thing called "delegation". That will let me host the actual server on matrix.zble.sk, but still use zble.sk as the name.

There are at least two ways to set this up. I'm using the "well-known" method: I'll host a simple JSON file at https://zble.sk/.well-known/matrix/server. When a Matrix server tries resolving a user's name with zble.sk in it, it will check that file and find where the actual Matrix server lives. This distinction is important in multiple places when setting up various components. If you do not need delegation, because your Synapse server lives on the same domain you want in your names, you can skip the .well-known setup step.

Varied gotchas

I wanted to build the entire setup from the ground up to avoid having to troubleshoot convoluted issues in software I don't know. That's why I decided to first make a non-delegated setup on a test server, then if it worked, set up the delegation. Only then I found out that this is impossible (because that would involve renaming the server) and I will have to rebuild the Synapse server. (I planned to do that anyway, since the first attempt was on a temporary test server, but it's something to keep in mind.)

Apparently Synapse also isn't very good at dropping connections to federated servers that stop responding, for example because they no longer exist, because they were just a test server which you deleted. πŸ˜€ That's why I've been asked to leave any federated chatrooms I have joined with my test account before dropping my test server, so the federated server knows there's no need to contact the test server anymore. ⚠

I caused myself a bit of a headache when setting up the first Synapse user. First I accidentally created a user without admin rights. Then I mistyped the password when creating a different one. The only way of solving this I found was making a third account, making it an admin and not messing up its password, logging with it into Synapse Admin and fixing everything from there. (And then deactivating all the superfluous users.)

The setup

These are the tasks I need to do:

The firewall

Short and easy:

nginx - delegation and proxying

There are two important bits: first one is setting up access to the .well-known folder that will enable delegation. This is done for the domain which will show up in usernames, as mentioned above.

I ended up with is:

server {
    root /var/www/html;
    server_name zble.sk;

    location / {
        # unrelated stuff - omitted

    location /.well-known {
        alias /var/www/webpath/.well-known;

Then I of course need to create the file in question, so I create the file .well-known/matrix/server and set this as its content:

    "m.server": "matrix.zble.sk"

The second important bit is adding server blocks for all the various compoents' subdomains. Might as well set them up all in one go. (Heads up: since some of those components are meant for my use only, I've changed the domain names and ports here.)

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

    server_name matrix.zble.sk;

    location / {
        proxy_pass http://localhost:33333;
        proxy_set_header X-Forwarded-For $remote_addr;
        client_max_body_size 400M;

server {
    listen 443 ssl;

    server_name synapseadmin.zble.sk;

    location / {
        proxy_pass http://localhost:33334;
        proxy_set_header X-Forwarded-For $remote_addr;
        client_max_body_size 400M;

server {
    listen 443 ssl;

    server_name element.zble.sk;

    location / {
        proxy_pass http://localhost:33335;
        proxy_set_header X-Forwarded-For $remote_addr;
        client_max_body_size 400M;


Note that no server listens at the unencrypted HTTP (80) port, and the Matrix server listens on the 8448 in additional to the standard SSL (443) port.

I've set the max client body size to 400MB everywhere, because I hate running into that limit. Someone eventually always tries to send a photo that's bigger than whatever-conservative-limit I had before. πŸ˜€

As always, after making any config changes, you should validate them by running nginx -t and if everything's OK, restart the nginx service with service nginx restart.

SSL certificates

That's entirely handled with Let's Encrypt's certbot. It can also update the nginx config, even redirect all HTTP traffic to HTTPS if you want (which you do). Certbot also schedules automatic certificate renewals.

sudo certbot --nginx -d matrix.zble.sk -d element.zble.sk -d synapseadmin.zble.sk

Configuring docker containers

So I ended up with this minimal docker-compose.yaml config:

version: "3"

    image: matrixdotorg/synapse:latest
    restart: unless-stopped
      - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
      - synapse_files:/data
      - 33333:8008

    image: awesometechnologies/synapse-admin:latest
    restart: unless-stopped
      - 33334:80

    image: vectorim/riot-web:latest
    restart: unless-stopped
      - riot_files:/app
      - 33335:80


Running docker-compose up --no-start will pull all the needed images and create everything (containers, volumes, networks...), but won't start them. This is handy, since we still have some configuring to do, first.

Configuring Synapse

We need to generate Synapse config.

  docker run -it --rm \
    --mount type=volume,src=matrix_synapse_files,dst=/data \
    -e SYNAPSE_SERVER_NAME=zble.sk \
    matrixdotorg/synapse:latest generate

Then we need to edit it to suit our needs. The easiest way is to find where the relevant files are located by finding the Mountpoint of the Synapse volume.

 ~> docker volume inspect matrix_synapse_files
        "CreatedAt": "2020-08-09T21:11:17Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "matrix",
            "com.docker.compose.version": "1.26.2",
            "com.docker.compose.volume": "synapse_files"
        "Mountpoint": "/var/lib/docker/volumes/matrix_synapse_files/_data",
        "Name": "matrix_synapse_files",
        "Options": null,
        "Scope": "local"

All operations in Docker's files will require root access, so either switch to root or prepend sudo as needed. We need to go to the Mountpoint (cd /var/lib/docker/volumes/matrix_synapse_files/_data) and then edit the homeserver.yaml file.

The file is quite large and you might want to set things up differently, but here are some of the keys I changed, with comments. I'm skipping over big chunks of the file that I just left on default values, so this snippet is not copy-pastable.

server_name: "zble.sk"
public_baseurl: https://matrix.zble.sk

This was a bit of a gotcha for me: the server_name should be the part which shows up in your usernames, but the public_baseurl is the URL of the Synapse instance. I didn't know the nomenclature well enough, got this wrong the first time around and nothing worked. πŸ™‚

# Is the preview URL API enabled?
url_preview_enabled: true
  - ''
  - ''
  - ''
  - ''
  - ''
  - ''
  - '::1/128'
  - 'fe80::/64'
  - 'fc00::/7'
# Whether or not to report anonymized homeserver usage statistics.
report_stats: false

# Configuration for sending emails from Synapse.
  smtp_host: smtp.mailgun.org 
  smtp_port: 587
  smtp_user: "mailgun username"
  smtp_pass: "mailgun secret"
  require_transport_security: true
  notif_from: "synapsenotificationusername@zble.sk"
  app_name: zble.sk

  # Uncomment the following to enable sending emails for messages that the user
  # has missed. Disabled by default.
  enable_notifs: true

# Uncomment to allow non-server-admin users to create groups on this server
enable_group_creation: true

These enable showing link previews in chat (an option I'd maybe like in the chat app, not the server, but whatever), disable stat reporting, set up e-mail notifications with Mailgun and allow non-admin users to create groups. That last one might sound a bit weird considering this server is intended only to have one user, but it will come in handy later, when setting up bridges.

Configuring Element (Riot)

We find the Element's config files in the same manner, by doing a docker volume inspect matrix_riot_files and going over to the Mountpoint directory. We need to edit the config.json file.

This one's a lot shorter. These are the most important (again, not presented in file order):

    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.zble.sk",
            "server_name": "zble.sk"
    "brand": "zble.sk",
    "default_federate": true,
    "default_theme": "dark",
    "roomDirectory": {
        "servers": [
   // ...

Starting up

That's it for the config. (Synapse Admin doesn't need any.) We can now start all the containers in headless mode with docker-compose up -d.

If everything worked as it should, all the URLs will now be live. The Element chat should load, and on accessing the Matrix server URL in a browser we should see this landing page:

That's all well and good, but we still have no way of actually logging in. The Synapse container has a script for creating a new user. Run it like this:

docker exec -it matrix_synapse_1 register_new_matrix_user http://localhost:8008 -c /data/homeserver.yaml	

⚠Heads up: the URL in there, can have a different port specified by default, and only print a confusing Connection refused error message. If that happens to you, change it to the internal 8008 port. And as mentioned in the Gotchas section above: don't make typos here. πŸ˜€

You should see something like this:

~/matrix> docker exec -it matrix_synapse_1 register_new_matrix_user http://localhost:8008 -c /data/homeserver.yaml
New user localpart [root]: zblesk
Confirm password:
Make admin [no]: yes
Sending registration request...

Congratulations! You can now go to your chat app and log in. If you go to your freshly-hosted Element instance, the URL to your Matrix server should already be filled in. If instead you use a native app on your phone or desktop, or some other app entirely, don't forget to specify your server URL as well as your credentials.

One potentially useful link: if you're not sure your federation is set up correctly, try the Federation tester.

What's next?

This is all we need to partake in the broad Matrix community. You can log into Element, click the "Explore rooms" and join anything in the federation. For example, try entering #outdoors:matrix.org. (The first join might take a moment.)

Or if you want to, you can message me at @zblesk:zble.sk.

But again, any chat platform, however cool, is only useful if it has people you want to talk to. So next up, a way to greatly increase Matrix's utility: bridges.