How to Run Matrix On a Raspberry Pi Without Opening Ports

The current state of the messaging industry is a disaster - a fragmented assortment of platforms who all refuse to integrate with any other. Now the EU’s impending Digital Markets Act will come into effect on November 2, but, until then, what can we use now to force interoperability?

Our savior comes in the form of Matrix, a federated messaging protocol a unique “bridging” feature to other services, such as Instagram, Telegram, Discord, and more! In other words, you can consolidate all your messages on a single Matrix account. It also happens to allow E2EE, though most bridges currently are not made to use it unfortunately.

The de facto server implementation of Matrix is called Synapse and is written in Python 🤮. There is an alternative server implementation called Dendrite that runs on Go, but it does not have full parity with Synapse yet. You may try this version, but your mileage will certainly vary.

We need somewhere to host this server at home, so we will be using a Raspberry Pi 4 8 GB model. I’m sure 4 GB will work fine as well, though I haven’t tested it myself. You may follow along with this guide if you’re using something else, though it steps may vary a little when installing the Matrix components.

Because we don’t want to open our ports (perhaps because of security concerns, CGNAT, etc.), we will be using Cloudflare (🤮) Tunnels to forward traffic to our Matrix Synapse server.

Of course, we could install Synapse manually, or we can have someone else do all the hard work - which is where the amazing matrix-docker-ansible-deploy project by spantaleev comes in!

Before we start, I will give you a small list of what we will be using:

  • matrix-docker-ansible-deploy - To install all the necessary Matrix components as well as any service bridges that you want.
  • Ansible - To run the matrix-docker-ansible-deploy playbook.
  • Synapse - The server implementation we will be using.
  • Element - The Matrix client that we will be using.
  • Cloudflare Tunnels - To forward traffic back and forth to our server without having to open any ports.
  • Let’s Encrypt - To get that beautiful HTTPS lock icon on our site (which is also required for Synapse to run).
  • Raspberry Pi 4B 8 GB - To run the Matrix homeserver on.
  • rpi-imager - To write Raspbian 64-bit OS to a microSD.

Prerequisites

  • A Cloudflare account to manage your DNS and create tunnels.
  • A domain to host your Matrix homeserver on.
  • A working server in your home to physically run Matrix on - we will be using the ARM64-based Raspberry Pi 4 for this guide.
  • A 32 GB or larger microSD card
  • A microSD card reader
  • A mouse and keyboard (optional if accessing solely through SSH)
  • A monitor (optional if accessing solely through SSH)

Step 0: Prepare your Raspberry Pi 4

We will need to get our Pi ready to install Matrix on it, let alone use it. If you already have a working Pi with a 64-bit OS running on it (e.g. Ubuntu Server - the default on the Pi 4 is 32-bit Raspbian), then skip to Step 1.

Step 0.1: Obtain a working Raspi 4

This is the most difficult step. Raspberry Pis are hard to come by during these trying times. But if you do happen to get one, keep reading on.

Step 0.2: Download 64-bit Raspbian

(OPTIONAL IF YOU ARE USING rpi-imager)

The default operating system on the Raspberry Pi 4 is 32-bit Raspbian. Synapse no longer builds Docker images for 32-bit systems, so we’re going to need to use the 64-bit image, which can be found on the Raspberry Pi site if you scroll down. The latest version as of writing is 2022-09-22-raspios-bullseye-arm64.img.xz. Extract the 64-bit Raspbian image from the .xz archive.

0.3: Install 64-bit Raspbian

At this point, you should plug in your microSD card reader with your >=32 GB microSD card.

Next, you will need some kind of disk imaging software, such as Balena Etcher or Rufus, however I will be using Raspberry Pi’s official imager.

In the official imager, we will select “Raspberry Pi OS (64-bit)”. If you don’t need the desktop, I recommend using the Lite version to reduce the load on your Pi. If you downloaded the image despite using the official imager, you can install it by scrolling down to the bottom and selecting “Use custom”.

Be VERY CAREFUL during this part: select your microSD card (it may show up as the reader) from the list of Storage options. Do not click on your computer’s hard drives unless you intend on wiping out your data with Raspbian.

Turing on SSH is necessary to use the Ansible playbook. If you would like to turn on SSH for your Pi without having to attach a monitor to your Raspberry Pi, please see Step 0.3.1.

Finally, select “Write” and wait for the imager to complete writing Raspbian to your microSD. Eject the card when finished and stick it in your Pi. Then, move on to Step 0.4.

0.3.1: Image Customization Options

(FOR rpi-imager USERS)

Before writing, we can set some settings for the Raspberry Pi image. We’ll want to enable SSH access so we .

For more security, I suggest using public-key authentication. If you already generated an SSH key, it will automatically fill the field with whatever was in ~/.ssh/id_rsa.pub. To generate a new key, you can do it on Linux using ssh-keygen and following the instructions. Copy public key in the file it generates (the one that ends with .pub).

For security, I recommend also changing the username and password for the Pi. If you are using Wi-Fi, you will want to configure Wireless LAN.

0.4: Turn on the Pi and update packages

Let the Pi boot up. If you are using SSH, please see Step 0.4.1, then return here.

After booting with the desktop environment, it may prompt you to add information. Plug in a mouse and keyboard and fill it out as asked. After this, it may install packages. This may take a hot minute. It will ask to restart when done.

Once the server has restarted or you have gained access, some packages in the image may not be updated to the latest version. In the interest of security, we’ll want to bring them all up to date by running the classic duo on the Raspberry Pi:

sudo apt update && sudo apt upgrade -y

Let it do its thing, then proceed to Step 1.

0.4.1: SSH into your Pi

(FOR THOSE WHO ENABLED SSH IN STEP 0.3.1)

To SSH into your Pi, you will need to find its IP address. This can be done with nmap using the command:

nmap 192.168.1.0/24

In the output, search for the device which has port 22 open. There may be multiple devices with this port open, so be careful. To check, you can try doing:

ssh [email protected]

where “pi” is the username you set for the Raspberry Pi (“pi” is the default) and 192.168.X.X is whatever IP address you think the Pi might be on.

It may warn you that the authenticity of the device cannot be verified. Type ‘yes’. Once it successfully logins, you may return to Step 0.4.

Step 1: Prepare matrix-docker-ansible-deploy

Now that we have the Pi finished, we will want to clone the matrix-docker-ansible-deploy repository to our computer.

You may also follow along with the official documentation.

Step 1.1: Clone matrix-docker-ansible-deploy

Open up terminal on your computer (not the Pi) and do:

git clone https://github.com/spantaleev/matrix-docker-ansible-deploy.git && cd matrix-docker-ansible-deploy

If you do not have git installed yet, install it now with:

sudo apt install git

Step 1.2: Configure your Matrix components

Within the matrix-docker-ansible-deploy directory, we want to create a new directory to hold all of our Ansible playbook configuration files.

mkdir inventory/host_vars/matrix.<your domain>

Then we want to grab a sample config file provided with the playbook.

cp examples/vars.yml inventory/host_vars/matrix.<your domain>/vars.yml

Open the file you just copied. Make sure to randomly generate secrets and keys with something like:

pwgen 64

Afterward, you can add whatever services you want to install. Please see the GitHub repository to see what you can install and how.

For the Raspberry Pi, though, there is one step that we MUST do: add the line:

matrix_architecture: arm64

to our config so we build and pull Docker images belonging to our current architecture. Please be aware that some of the bridges and tools in the Ansible playbook may be for x86_64 only! Check with each project to make sure it can run on arm64 systems.

Step 1.3: Configure your Matrix hosts file

Return to the matrix-docker-ansible-deploy directory. Now we want to configure the what host this playbook to install its components on. Copy the example hosts file provided by the playbook.

cp examples/hosts inventory/hosts

Now edit inventory/hosts. At the bottom, it should look like:

[matrix_servers]
matrix.<your-domain> ansible_host=<your-server's external IP address> ansible_ssh_user=root
  • Replace <your-domain> with the domain you are hosting Matrix on.
  • Replace <your-server's external IP address> with the IP address of your Raspberry Pi. If you do not know your Raspberry Pi’s IP address, you can find it using either ifconfig on the Pi itself or using the instructions in Step 0.4.1.
  • Replace root with the username of your Pi user and append become=true become_user=root.
  • Also append the text below to the end. We setting up Cloudflare Tunnel in a bit, just hang on and come back when you’re done.
ansible_ssh_pipelining=False matrix_coturn_turn_external_ip_address=<cloudflare tunnel address>

Step 2: Configuring Cloudflare with Tunnels

Step 2.1: Setup Cloudflare DNS

If you haven’t already, go to Cloudflare, register an account, and add your domain on their site. Go to your domain registrar and replace the nameservers for your domain with the ones provided by Cloudflare.

Step 2.2: Install cloudflared

Go back to your Raspberry Pi. In its terminal, we need to get cloudflared. Unfortunately, it is not found in the apt repository, so we will have to download it ourselves using the command:

wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb

If you are using and x86_64/amd64 system, replace arm64 with amd64.

With the package downloaded on our Raspberry Pi, we can install it by writing:

dpkg  cloudflared-linux-arm.deb

Step 2.3: Create a Cloudflare Tunnel

To create a persistent tunnel, we need to log into Cloudflare.

cloudflared tunnel login

Click the link after providing your credentials. You may need to CTRL+Left click on some terminals. After following the instructions provided, it will generate a cert.pem in the /etc/cloudflared directory.

Now we create our tunnel with:

cloudflared tunnel create <whatever name you want>

It will output a Cloudflare Tunnel UUID. Remember it.

Step 2.4: Configure the tunnel

Create and edit the tunnel config with:

sudo nano /etc/cloudflared/config.yml

You will want to create a configuration like this for now:

tunnel: <TUNNEL UUID>
credentials-file: /home/<USERNAME>/.cloudflared/<TUNNEL UUID>.json
originRequest:
    originServerName: <YOUR DOMAIN>

ingress:
  - hostname: "*.<YOUR DOMAIN>"
    service:  http://localhost:80
  - service:  http_status:404

We are setting http://localhost:80 for now so Let’s Encrypt can verify that we own the domains and give us SSL.

Step 2.5: Create CNAME records for your Matrix domains

You may use the Cloudflare dashboard or CLI to create CNAME records.

In the CLI, we can run this command:

cloudflared tunnel route dns <UUID or NAME> <DOMAIN>

In <DOMAIN>, you will want to put the following:

  • <YOUR DOMAIN>
  • matrix.<YOUR DOMAIN>
  • element.<YOUR DOMAIN>

Lastly, we can use this command to start our tunnel:

cloudflared tunnel run <TUNNEL UUID or NAME>

Make sure to go back to Step 1.3 and add your tunnel UUID.

Step 2.6: Set up cloudflared as a service

Using systemctl, we can run cloudflared as a service so we can run it in the background.

cloudflared service install && systemctl start cloudflared

Step 3: Run the Ansible playbook

Now we can get around to generating our Matrix services. Return to your computer.

Step 3.1: Install Ansible

If you haven’t already, you will need to install Ansible on your computer (again, not the Pi). You will need an ansible-core version higher than 2.7.1 according to the playbook, but I would install the latest. You will not be able to install the latest from the default app respositories, so we will add Ansible’s PPA.

sudo apt-add-repository ppa:ansible/ansible && sudo apt update

Then we install Ansible.

sudo apt install ansible

Step 3.2: Set up the playbook

cd back into the matrix-docker-ansible-deploy directory and run the playbook command:

ansible-playbook -i inventory/hosts setup.yml --tags=setup-all

And now wait another hot minute for everything to get set up for you.

Step 4: Configure Cloudflare tunnels (again) for SSL

Unfortunately, you will have to change the Cloudflare Tunnel port from 80 to 443 and back every time your SSL certificates are up for renewal. I have not found a way within the playbook to automatically renew SSL with Cloudflare Tunnels beyond just manually using certbot.

Change service in:

  - hostname: "*.<YOUR DOMAIN>"
    service:  http://localhost:80

to:

  - hostname: "*.<YOUR DOMAIN>"
    service:  https://localhost:443

Save then restart the tunnel by running:

sudo systemctl restart cloudflared

Step 5: Start Matrix services with the playbook

And now, the moment of truth!

ansible-playbook -i inventory/hosts setup.yml --tags=start

Once this command successfully finishes, we can finally access our Matrix homeserver using the Element client at element.<your domain>. Woo hoo!

But how do we make an account?

Step 6: Post-Installation

Creating an account

Using the Ansible playbook, we can create our first account.

ansible-playbook -i inventory/hosts setup.yml --extra-vars='username=<your-username> password=<your-password> admin=<yes|no>' --tags=register-user

By default, you are not allowed to create an account on your homeserver. This is clearly to prevent random people from creating accounts on your homeserver. If you want randos joining your homeserver, then you can enable it in your vars.yml that you created earlier in Step 1.2 by adding:

matrix_synapse_enable_registration: true

Token-based authentication

If you want to allow people to register themselves, but still maintain control on who can register, you can use registration tokens.

In vars.yml, insert these config options:

matrix_synapse_enable_registration_without_verification: true
matrix_synapse_registration_requires_token: true

To create these tokens, we’re going to need either Synapse Admin or matrix-registration-bot. This guide will use the bot.

Add these to your vars.yml.

matrix_bot_matrix_registration_bot_enabled: true
matrix_bot_matrix_registration_bot_bot_access_token: <create a new user and get their access token>

You will need to create a new user to serve as a bot. In Element, the access token can be found by going to “All Settings”, “Help & About”, then clicking “Access Token” under “Advanced” to reveal the currently-logged-in user’s access token.

Federation

I have not tried federating with another server yet, but I have gotten the Federation Tester to work.

You will need to create another CNAME in your DNS for this to work. This site uses the CNAME matrix-federation.wenkdth.org for federation.

In your vars.yml, write:

matrix_server_fqn_matrix_federation: "matrix-federation.{{ matrix_domain }}"
matrix_federation_public_port: 8448

Do not touch {{ matrix_domain }}, that is a variable that will automatically put whatever you set matrix_domain to there.

Since Synapse will be listening to port 8448 for federation traffic, we will need to edit our Cloudflare Tunnel to forward traffic there.

Under ingress:, append:

  - hostname: matrix-federation.<YOUR DOMAIN>
    service:  https://localhost:8448
    originRequest:
       originServerName: matrix.<YOUR DOMAIN>

I’m not sure why originServerName works, but it does.

Make sure to save then restart the Cloudflare Tunnel.

Now we need to do something a little messy. Go into /home/locke/matrix-docker-ansible-deploy/roles/matrix-base/templates/static-files/well-known and edit matrix-server.j2 to:

#jinja2: lstrip_blocks: "True"
{
	"m.server": "{{ matrix_server_fqn_matrix_federation }}:443"
}

This allows traffic to point to the right area. Simply setting the federation port in vars.yml to 443 will not work since that port is taken by Synapse.

If you would like to test your domain’s federation, use the try out the Matrix Federation Tester. Make sure you use your base domain (not matrix.<your domain>).

If you want to set a whitelist of federated serves (so you don’t have people joining massive Matrix.org rooms and killing your poor Raspberry Pi), you can use something along the lines of:

matrix_synapse_federation_domain_whitelist:
 - mozilla.modular.im
 - halogen.city
 - kde.org
 - matrix.tchncs.de

Add and remove domains as you wish.

You may also limit joining federated rooms by complexity with:

matrix_synapse_configuration_extension_yaml: |
 limit_remote_rooms:
   enabled: true
   complexity: 1.0 

Higher complexity means you’re going to have to deal with bigger rooms, thus more RAM.

Step 7: Invite people to join your server!

So now you’ve got a working Matrix server! Tell your friends about this article, maybe challenge them to set up their own Matrix server to federate with! The fediverse is your oyster!

Expect this article to be updated now and then for any big errors I might have made :).