zurück / back

Host your own Matrix instance

We need to take back the internet!

The far-right just had a huge success in the German elections. I'm a 51 year old white hetero cis* male in a quite solid financial situation. And I'm very concerned. From a statistically point of view, you probably have much more reason to be scared.

I'm an IT person, so my best options to help resisting the right shift is via technology. Free and secure communication is a required building block for a working democracy. Let us all move to Matrix:

Matrix is an open protocol for decentralised, secure communications.

We believe:

I'm experimenting with my own Matrix instance already for a while. My goal was to come up with the perfect guide for setting up your own small personal instance. Due to time constraints this will probably never happen. But we need to get started. So lets do it like in the good old days of the internet: I'll share my current not-yet-perfect experiences. You can either learn from them or correct me. Either way we will learn and improve together in public. Lets got!

The server

I choose Hetzner as my provider for the server. They are a German company, so they fall under EU regulations. I also appreciate their very cheap storage boxes for backup. But every Linux server will do the job.

I use Ubuntu because that's what I'm used to. The software will anyhow be run via Docker. Just make sure that your system is up-to-date and install Docker following the docs.

You need a dedicated second level domain for the following setup. Mine is plaudern.social ("plaudern" is "to chat" in German). You'll need to subdomains. You can choose the name as you like. I use the following ones:

Folder structure

I want to have the whole setup in one folder, but with clear separation between:

My resulting structure looks like this:

├── .env
├── compose.yaml
├── configs
│   ├── caddy
│   └── synapse
├── data
│   ├── caddy
│   ├── postgres
│   ├── synapse
├── logs
└── static
    ├── element
    └── synapse-admin

The static folder contains the static content for the Element web UI and the admin interface. They are mostly just downloaded and extracted into that place. Lets ignore some minor details until later.

compose.yaml

The compose.yaml defines which containers need to run. Before we go into details a few important remarks:

Now we are ready to have a look at the individual containers.

Postgresql

The first thing we need is a Postgres database. Here is the snippet from the compose.yaml:

db:
  user: 1234:1234
  image: docker.io/postgres:17-alpine
  restart: 'unless-stopped'
  environment:
    - POSTGRES_USER=postgres_admin
    - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
  volumes:
    - ./data/postgres:/var/lib/postgresql/data
    - /etc/passwd:/etc/passwd:ro

All of this should be pretty straight forward. The POSTGRES_INITDB_ARGS is from rather old Synapse docs, so I'm not sure whether those settings are still needed, but the work fine so far.

The last line is need due to the way Postgres starts its process. Without mapping this file into the container the user could be resolved and I got strange access problems.

Start your Postgres container:

docker compose up db

From another terminal run:

docker compose exec db psql -U postgres_admin

Create a database called synapse and a user synapse with a secure password.

Synapse

Synapse is the default server implementing the Matrix protocol. The snippet for the compose.yaml is also pretty straight forward.

synapse:
  user: 1234:1234
  image: docker.io/matrixdotorg/synapse:latest
  restart: unless-stopped
  environment
    - SYNAPSE_CONFIG_DIR=/config
  volumes:
    - ./configs/synapse:/config
    - ./data/synapse:/data
    - ./logs/synapse:/logs
  depends_on:
    - db
  ports:
    - 8448:8448/tcp

The actuall configuration happens in the homeserver.yml that lives in the /config folder. We will come back to it in a moment.

Caddy

Now for the probably most important advice in this whole document: Don't use Nginx! It is a great tool, but not for this job. I spend hours trying to get the certificate stuff setup and failed miserably. I had to dig into Github issues to learn that a script I considered "official" is actually unmaintained since ages. Caddy just works and takes care of the certificates out of the box. Here is the snippet:

caddy:
  user: 1234:1234
  image: caddy:2
  restart: unless-stopped
  ports:
    - "80:80"
    - "443:443"
    - "443:443/udp"
  volumes:
    - ./configs/caddy:/etc/caddy
    - ./data/caddy/data:/data
    - ./data/caddy/config:/config

    - ./static/element:/static/element
    - ./static/synapse-admin:/static/synapse-admin

That's it. These three containers are all it needs to run your own Matrix instance. A few config files are still missing. Here they are ...

Config files

Synapse

Most of the config is copy & paste straight from the docs. Most notable changes:

report_stats: true
suppress_key_server_warning: true
macaroon_secret_key: "..."
registration_shared_secret: "..."

public_baseurl: https://plaudern.social/
serve_server_wellknown: true

server_name: "plaudern.social"
pid_file: /data/homeserver.pid
listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    bind_addresses: ['::1', '0.0.0.0']
    resources:
      - names: [client, federation]
        compress: false
database:
  name: psycopg2
  args:
    user: synapse
    password: "..." 
    dbname: synapse
    host: db
    cp_min: 5
    cp_max: 10
log_config: "/config/plaudern.social.log.config"
media_store_path: /data/media_store
signing_key_path: "/data/plaudern.social.signing.key"
trusted_key_servers:
  - server_name: "matrix.org"

The file contains some secrets. I struggled to get the out of this file. Only a day ago somebody told me that they could be set via environment variables. I'll update the article as soon as I tested it.

Caddy

The config for Caddy is even simpler:

plaudern.social {
  reverse_proxy /_matrix/* synapse:8008
  reverse_proxy /_synapse/client/* synapse:8008
  reverse_proxy /_synapse/admin/* synapse:8008
  reverse_proxy synapse:8008
}

chat.plaudern.social {
  root * /static/element
  file_server
}

admin.plaudern.social {
  root * /static/synapse-admin
  file_server
}

Yes, that's all. The two subdomains just serve static content. Download it from the projects mentioned above, extract them. Element contains a template for config.json. Rename it and adapt the following snippet according to your domain names:

    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://plaudern.social",
            "server_name": "plaudern.social"
        },
        // ...

Backup

For a personal instance is don't care too much about availabilty and fail over. Life will go on if my instance goes away for "a moment". But the data should be safe. So the missing step is an automated backup procedures. The relevant parts are:

The Element backups boils down to a database dump and backups of certain folders in the file system. I plan to setup Borg to ship backups to my Hetzner Storage Box. But that will be a follow up article.

Next steps

As mentioned above, this article is written in a hurry and should be considered work in progress. It would never got published, if I would aim for perfection. The goal is to get started, share knowledge and learn together in public. If you have questions, improvements, ... don't hesitate to contact me.

Impressum