Twelve builds

A Matrix homeserver whose federation actually works

Synapse on a small Ryzen instance with Postgres, delegation done through well-known files rather than SRV records, and verification that reaches other servers.

What this builds

A homeserver at example.com, running on matrix.example.com, federating properly with everyone else. Postgres underneath, nginx in front, delegation handled by two small JSON files, and registration closed so that your server does not become somebody’s free relay within a week.

Federation is where these installations usually fail, and almost never because of Synapse. The server runs, local messages work, and then nothing from outside arrives. In practice the cause is nearly always delegation: the identity of the server and the address it lives at are two different things, and the mechanism connecting them is easy to get subtly wrong.

Before you start

  • An R-4 is enough for a few dozen users. Synapse is memory-hungry rather than CPU-hungry, and joining large federated rooms is the one thing that will make it sweat.
  • Two names in DNS: example.com, which is your server’s identity and appears in every user id, and matrix.example.com, which is where the software actually listens.
  • Certificates for both. The first only needs to serve two static files.

1. Database, with the right collation

apt update && apt install -y postgresql matrix-synapse nginx certbot
sudo -u postgres psql -c "CREATE USER synapse_user WITH PASSWORD '<a long password>';"
sudo -u postgres psql -c "CREATE DATABASE synapse ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE=template0 OWNER synapse_user;"

The collation is not a detail. Synapse refuses to start on a database created with any collation other than C, and the error it prints when you get this wrong arrives after you have already imported data. Do it now, correctly, once.

2. Homeserver configuration

Debian’s package asks for the server name during installation; answer example.com, not the hostname of the machine. That answer becomes part of every user id and room id on the server and cannot be changed afterwards without abandoning the server.

Then edit /etc/matrix-synapse/homeserver.yaml:

server_name: "example.com"
public_baseurl: "https://matrix.example.com/"
pid_file: /run/matrix-synapse.pid

listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    bind_addresses: ['127.0.0.1']
    resources:
      - names: [client, federation]
        compress: false

database:
  name: psycopg2
  args:
    user: synapse_user
    password: "<a long password>"
    database: synapse
    host: 127.0.0.1
    cp_min: 5
    cp_max: 10

enable_registration: false
registration_shared_secret: "<a long random string>"
report_stats: false
suppress_key_server_warning: true
url_preview_enabled: false

media_store_path: /var/lib/matrix-synapse/media
max_upload_size: 100M
media_retention:
  remote_media_lifetime: 30d

rc_message:
  per_second: 0.5
  burst_count: 15

Two of those settings will save you disk and grief. Remote media retention discards cached copies of other servers’ files after a month, and without it your media store grows forever with content you never asked for. URL previews are off because they make your server fetch arbitrary addresses on behalf of anyone who can post a link, which is a request forgery engine with a chat interface attached.

x_forwarded: true matters as well: without it Synapse rate-limits every user in the world as though they were one client, because every request appears to come from nginx.

3. Delegation

Your identity is example.com and your server is at matrix.example.com. Two files bridge the gap. Serve them from example.com, over HTTPS, with the right content type.

/.well-known/matrix/server:

{ "m.server": "matrix.example.com:443" }

/.well-known/matrix/client:

{ "m.homeserver": { "base_url": "https://matrix.example.com" } }

The port in the first file is mandatory. Omit it and other servers fall back to port 8448, find nothing listening, and give up quietly; your users then report that federation is broken while every log on your machine looks perfectly healthy.

server {
  listen 443 ssl;
  server_name example.com;
  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  location /.well-known/matrix/ {
    root /var/www/wellknown;
    default_type application/json;
    add_header Access-Control-Allow-Origin *;
  }
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name matrix.example.com;
  ssl_certificate     /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
  client_max_body_size 100m;

  location ~ ^(/_matrix|/_synapse/client) {
    proxy_pass http://127.0.0.1:8008;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
  }
}

The Access-Control-Allow-Origin header on the well-known location is required by web clients and forgotten by roughly everybody. Without it, desktop clients work and browser clients cannot find your server at all.

mkdir -p /var/www/wellknown/.well-known/matrix
systemctl restart matrix-synapse nginx

4. A user

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

Answer the prompts, make the first account an admin, and leave open registration off. A homeserver with open registration is a spam source within days, and other servers will start refusing your traffic long before you notice.

Verify it

Work outwards. First, is the software answering at all:

curl -s https://matrix.example.com/_matrix/client/versions | head -c 200
curl -s https://matrix.example.com/_matrix/federation/v1/version

Then, is delegation being served correctly, with the port present and the content type right:

curl -si https://example.com/.well-known/matrix/server | grep -E "content-type|m.server"
curl -s https://example.com/.well-known/matrix/client

Then, does your signing key resolve. Other servers fetch this before they will talk to you at all:

curl -s https://matrix.example.com/_matrix/key/v2/server | head -c 300

Finally, the only test that counts. Log in with a client, join a public room hosted on a homeserver that is not yours, and send a message. Then read the federation state from the admin API, using the access token your client shows in its settings:

curl -s -H "Authorization: Bearer <admin token>" \
  "https://matrix.example.com/_synapse/admin/v1/federation/destinations" | head -c 400

Each destination should show a recent successful transaction and no retry interval. Destinations with a growing retry_interval are servers you cannot reach; if every destination looks like that, the problem is delegation rather than the network, and step three is where to look.

journalctl -u matrix-synapse --since "10 min ago" | grep -i "federation" | tail -20

Afterwards

Joining a very large room the first time will pin a core for several minutes and pull in a great deal of state. That is normal, it happens once per room, and it is the main reason people conclude Synapse is slow. Voice and video need a TURN server alongside this one; coturn on the same instance handles a small community, though it wants its own UDP port range in the firewall. Federation is chatty in both directions, which is worth remembering when picking the site: our location index lists the round-trip times we measure ourselves.

Ready when you are

Pick a city. Pick a size. Pay in coin.

No forms about who you are, no wait for a human to approve you, no phone call to verify anything. The invoice clears and the credentials land in your inbox.