Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen …

Anwendungen Schwierigkeit: Profi

Matrix: der eigene Chat-Server mit Synapse (Teil 1)

Synapse 1.164 mit Postgres 18 hinter Traefik: Server-Name, Delegation, geschlossene Registrierung – und der Föderationstest, der am Ende grün sein muss.

· 11 Min. Lesezeit ·Dauer: ca. 90 Minuten
Inhaltsverzeichnis

Ein eigener Chat-Server, der mit dem Rest der Matrix-Welt reden kann: Deine Nachrichten liegen auf deinem Server, und trotzdem schreibst du Leuten auf matrix.org oder bei ihrem eigenen Server. Der Preis dafür sind ein paar Entscheidungen, die man am Anfang trifft und später nur schwer ändert.

Was bauen wir?

Wir betreiben Synapse 1.164.2 mit PostgreSQL 18.6 hinter Traefik. Am Ende hast du einen Matrix-Homeserver mit geschlossener Registrierung, einem eigenen Benutzerkonto und einer funktionierenden Föderation. Dass sie funktioniert, belegt der offizielle Föderationstester.

Dieses Tutorial ist Teil 1 und behandelt bewusst nur den Server. Element Web als Oberfläche, der Alltag mit Räumen und die Administration kommen in Teil 2, sonst würde daraus ein Buch.

Eine Entscheidung, die bleibt

Der server_name wird Teil jeder Benutzer-ID (@du:DEINE_DOMAIN) und jeder Raumadresse (#kochabend:DEINE_DOMAIN). Ihn nachträglich zu ändern ist praktisch unmöglich: Alle Konten wären andere, die Föderation verlöre jede Zuordnung. Überleg dir vor dem ersten Start, welche Domain du dauerhaft willst. Am besten kurz und schön, denn sie steht später hinter jedem Namen.

Voraussetzungen

  • Traefik als Reverse Proxy mit funktionierendem Let’s-Encrypt-Resolver
  • Eine Domain, die auf den Server zeigt (A- und idealerweise AAAA-Record)
  • Mindestens 2 GB Arbeitsspeicher. Frisch gestartet und ohne Last braucht Synapse gut 100 MB und Postgres rund 60 MB; sobald du Räumen mit vielen Teilnehmern beitrittst, wächst der Bedarf deutlich
🍳 Empfehlung Anzeige

VPS 2000 G12.5

8 vCore · 16 GB RAM · 256 GB SSD

ab 26,92 €/Monat

Matrix wächst mit den Räumen, denen du beitrittst – der größere VPS ist hier keine Verschwendung.

Zu netcup →

💶 1 Monat gratis:

Einmal einlösbar, gilt für VPS 2000 G12.5. Im Warenkorb einlösen →

💶 5,95 € Gutschein für netcup-Neukunden: immer gültig · nicht für Domains und VPS Lite

Alle netcup-Gutscheine →

Schritt für Schritt

Schritt 1: Server-Name, Host und Delegation auseinanderhalten

Matrix kennt zwei verschiedene Adressen, und das ist die häufigste Verwirrung am Anfang:

  • Der server_name steht in den IDs: @koch:DEINE_DOMAIN. Er ist reine Namensgebung.
  • Der Host, auf dem Synapse tatsächlich läuft, kann ein ganz anderer sein, etwa matrix.DEINE_DOMAIN.

Damit fremde Server den zweiten finden, wenn sie nur den ersten kennen, gibt es Delegation: Unter https://DEINE_DOMAIN/.well-known/matrix/server liegt eine kleine JSON-Datei, die auf den echten Host zeigt. Genauso findet ein Chat-Programm über https://DEINE_DOMAIN/.well-known/matrix/client, wohin es sich verbinden soll.

In dieser Anleitung laufen server_name und Host auf derselben Domain. Das ist der einfachste Fall, und Synapse kann die beiden Dateien dann selbst ausliefern. Einen zusätzlichen Webserver brauchst du nicht. Willst du später auf matrix.DEINE_DOMAIN umziehen, bleibt der server_name gleich und nur die Delegationsdatei ändert sich.

Schritt 2: Die Konfiguration erzeugen lassen

Synapse schreibt sich seine Grundkonfiguration selbst, inklusive der Geheimnisse und des Signaturschlüssels:

Terminal
mkdir -p /opt/matrix/data && cd /opt/matrix
docker run --rm -v /opt/matrix/data:/data \
  -e SYNAPSE_SERVER_NAME=DEINE_DOMAIN \
  -e SYNAPSE_REPORT_STATS=no \
  ghcr.io/element-hq/synapse:v1.164.2 generate
Ausgabe
Creating log config /data/DEINE_DOMAIN.log.config
Setting ownership on /data to 991:991
Generating config file /data/homeserver.yaml
Generating signing key file /data/DEINE_DOMAIN.signing.key
A config file has been generated in '/data/homeserver.yaml' for server name 'DEINE_DOMAIN'. Please review this file and customise it to your needs.

Der Signaturschlüssel ist unersetzlich

Die Datei DEINE_DOMAIN.signing.key ist die Identität deines Servers gegenüber allen anderen Matrix-Servern. Geht sie verloren, hält dich das restliche Netz für einen Hochstapler und lehnt deine Events ab. Sie gehört vom ersten Tag an ins Backup, nicht erst, wenn Daten drin sind. Dasselbe gilt für registration_shared_secret, macaroon_secret_key und form_secret aus der homeserver.yaml.

Die erzeugte Datei benutzt SQLite. Für alles außer einem Einzelplatz-Test ist das zu wenig; wir stellen in der homeserver.yaml auf PostgreSQL um und ergänzen die Einstellungen, die diesen Server betreffen:

YAML
# /opt/matrix/data/homeserver.yaml (Auszug – Geheimnisse bleiben unangetastet)
listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    bind_addresses: ['0.0.0.0']
    resources:
      - names: [client, federation]
        compress: false

database:
  name: psycopg2
  args:
    user: synapse
    password: DEIN_DB_PASSWORT
    dbname: synapse
    host: db
    port: 5432
    cp_min: 5
    cp_max: 10

public_baseurl: https://DEINE_DOMAIN/
serve_server_wellknown: true
enable_registration: false
suppress_key_server_warning: true
max_upload_size: 50M
url_preview_enabled: false

Vier Zeilen verdienen eine Erklärung:

  • x_forwarded: true: Synapse liest die echte Besucher-IP aus dem X-Forwarded-For-Header, den Traefik setzt. Ohne das steht in jedem Protokoll die Adresse des Proxys. Die erzeugte Datei hat den Schalter schon gesetzt; lass ihn beim Umbau stehen.
  • public_baseurl: Daraus baut Synapse die Client-Delegation und alle Links in Einladungen.
  • serve_server_wellknown: true: Synapse liefert /.well-known/matrix/server selbst aus. Das ist der Grund, warum wir keinen zweiten Webserver brauchen.
  • url_preview_enabled: false: Die Vorschau von Links lässt deinen Server jede URL abrufen, die jemand in einen Raum schreibt. Das ist eine hübsche Funktion und gleichzeitig eine Möglichkeit, deinen Server als Abrufwerkzeug für fremde Adressen zu missbrauchen. Lass sie zunächst aus.

Schritt 3: PostgreSQL mit der richtigen Sortierung

Synapse verlangt eine Datenbank mit der Sortierreihenfolge C. Der Grund ist unspektakulär und die Folge unangenehm: Mit einer sprachabhängigen Sortierung wie de_DE.utf8 vergleicht Postgres Zeichenketten anders, und Synapse verweigert deshalb von vornherein den Start.

Im Docker-Image erreichst du das über POSTGRES_INITDB_ARGS – beim allerersten Start, denn danach steht die Sortierreihenfolge fest:

YAML
    environment:
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"

Nachsehen kannst du das jederzeit:

Terminal
docker compose exec db psql -U synapse -d synapse \
  -c "SELECT datname, pg_encoding_to_char(encoding), datcollate FROM pg_database WHERE datname='synapse';"
Ausgabe
 datname | pg_encoding_to_char | datcollate
---------+---------------------+------------
 synapse | UTF8                | C

Schritt 4: Der Stack

YAML
# /opt/matrix/compose.yaml
services:
  db:
    image: postgres:18.6-alpine
    container_name: matrix-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
    volumes:
      - pgdata:/var/lib/postgresql
    networks: [matrix-intern]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U synapse"]
      interval: 10s
      timeout: 5s
      retries: 6

  synapse:
    image: ghcr.io/element-hq/synapse:v1.164.2
    container_name: synapse
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      SYNAPSE_CONFIG_PATH: /data/homeserver.yaml
      TZ: Europe/Berlin
    volumes:
      - ./data:/data
    networks: [matrix-intern, proxy]
    labels:
      - traefik.enable=true
      - traefik.http.routers.synapse.rule=Host(`DEINE_DOMAIN`)
      - traefik.http.routers.synapse.entrypoints=websecure
      - traefik.http.routers.synapse.tls.certresolver=le
      - traefik.http.services.synapse.loadbalancer.server.port=8008

volumes:
  pgdata:

networks:
  matrix-intern:
    internal: true
  proxy:
    external: true

Die Datenbank liegt in einem Netz mit internal: true – von außen und aus dem proxy-Netz nicht erreichbar. Das Datenbankpasswort steht in einer .env daneben und muss mit dem Wert in der homeserver.yaml übereinstimmen.

Hinweis

Der Mount der Datenbank liegt auf /var/lib/postgresql und nicht auf /var/lib/postgresql/data. Ab PostgreSQL 18 verweigert der Container mit dem alten Pfad den Start, auch bei leerem Volume.
Terminal
docker compose up -d
docker compose ps
Ausgabe
NAME        STATUS
matrix-db   Up 55 seconds (healthy)
synapse     Up 45 seconds (healthy)

Beim ersten Start legt Synapse das Schema an und fährt anschließend Hintergrund-Migrationen. In den Logs stehen dann minutenlang Zeilen wie Running background update 'threads_backfill'. Das ist normal und stört den Betrieb nicht.

Schritt 5: Die vier Endpunkte, die stimmen müssen

Bevor du irgendein Chat-Programm anfasst, prüfst du von außen vier Adressen. Zuerst, ob die Client-API überhaupt antwortet:

Terminal
curl -s https://DEINE_DOMAIN/_matrix/client/versions
Ausgabe
{"versions":["r0.0.1","r0.1.0", … ,"v1.16","v1.17"],"unstable_features":{ … }}

Dann die beiden Delegationsdateien, die Synapse dank serve_server_wellknown selbst ausliefert:

Terminal
curl -s https://DEINE_DOMAIN/.well-known/matrix/server
curl -s https://DEINE_DOMAIN/.well-known/matrix/client
Ausgabe
{"m.server":"DEINE_DOMAIN:443"}
{"m.homeserver":{"base_url":"https://DEINE_DOMAIN/"}}

Und schließlich der Schlüsselendpunkt, über den fremde Server die Signaturen deines Servers prüfen:

Terminal
curl -s https://DEINE_DOMAIN/_matrix/key/v2/server
Ausgabe
{"old_verify_keys":{},"server_name":"DEINE_DOMAIN","signatures":{"DEINE_DOMAIN":{"ed25519:a_udcw":" … "}}, … }

Zum Schluss die Kontrolle, dass die Tür zu ist:

Terminal
curl -s -X POST https://DEINE_DOMAIN/_matrix/client/v3/register -d '{}'
Ausgabe
{"errcode":"M_FORBIDDEN","error":"Registration has been disabled"}

Offene Registrierung ist keine Option

Ein Matrix-Server mit offener Registrierung wird zuverlässig innerhalb von Tagen für Spam missbraucht. Dann landet dein server_name auf Sperrlisten anderer Server, und das bekommst du nur schwer wieder los. enable_registration: false bleibt, und Konten legst du von Hand an.

Schritt 6: Das erste Konto anlegen

Weil die Registrierung zu ist, benutzt du das mitgelieferte Werkzeug. Es authentifiziert sich über das registration_shared_secret aus der Konfiguration, -a macht den Benutzer zum Administrator:

Terminal
docker compose exec synapse register_new_matrix_user \
  -u DEIN_BENUTZERNAME -p DEIN_PASSWORT -a \
  -c /data/homeserver.yaml http://localhost:8008
Ausgabe
Sending registration request...
Success!

Schritt 7: Gegenprobe über die Client-API

Jetzt der eigentliche Funktionstest, ohne Chat-Programm und nur mit curl. Erst anmelden:

Terminal
curl -s -X POST https://DEINE_DOMAIN/_matrix/client/v3/login \
  -H "Content-Type: application/json" \
  -d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"DEIN_BENUTZERNAME"},"password":"DEIN_PASSWORT"}'

Die Antwort enthält user_id, device_id und einen access_token. Die Benutzer-ID zeigt, dass der server_name sitzt:

Ausgabe
"user_id":"@DEIN_BENUTZERNAME:DEINE_DOMAIN"

Mit dem Token legst du einen Raum an, schickst eine Nachricht hinein und liest sie wieder aus. Damit ist die komplette Kette aus Client-API, Datenbank und Ereignisspeicher belegt:

Terminal
curl -s -X POST https://DEINE_DOMAIN/_matrix/client/v3/createRoom \
  -H "Authorization: Bearer DEIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Kochabend","preset":"private_chat"}'
Ausgabe
{"room_id":"!68AeABE_Q59hjgJkh8wIl3zdGbp_n7k4a9uOf4yDrF0"}

Die Raum-ID trägt keinen Servernamen: Synapse legt neue Räume in Raumversion 12 an, und deren IDs sind reine Prüfsummen. Was es damit auf sich hat, steht in Teil 2.

Terminal
curl -s -X PUT "https://DEINE_DOMAIN/_matrix/client/v3/rooms/DEINE_RAUM_ID/send/m.room.message/txn1" \
  -H "Authorization: Bearer DEIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"msgtype":"m.text","body":"Zwiebelkuchen um 19:15 Uhr"}'
Ausgabe
{"event_id":"$WHceSNXyP34qT0CoQjq0E97dm7p7ZVLArH7mZdKftyA"}

Schritt 8: Föderation prüfen

Alles bisher ging um deinen Server allein. Ob der Rest der Matrix-Welt mit ihm reden kann, beantwortet der offizielle Föderationstester. Er löst deine Delegation auf, baut eine Verbindung auf, prüft das Zertifikat und vergleicht den Servernamen mit der Signatur:

Terminal
curl -s "https://federationtester.matrix.org/api/report?server_name=DEINE_DOMAIN" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('FederationOK:', d['FederationOK'])"
Ausgabe
FederationOK: True

Der Prüfer führt beide Adressen (IPv4 und IPv6) einzeln auf; bei beiden muss ValidCertificates: True und MatchingServerName: True stehen. Genau das willst du sehen, bevor du jemandem deine Matrix-Adresse gibst. Dieselbe Prüfung gibt es auch als Weboberfläche unter federationtester.matrix.org; die API eignet sich besser, um sie später gelegentlich zu wiederholen.

Schritt 9: Die Admin-API abschotten

Synapse bringt eine eigene Verwaltungs-API mit, die unter /_synapse/admin/ liegt. Mit dem Token deines Administratorkontos siehst du damit zum Beispiel alle Konten:

Terminal
curl -s "https://DEINE_DOMAIN/_synapse/admin/v2/users?from=0&limit=5" \
  -H "Authorization: Bearer DEIN_TOKEN"
Ausgabe
total: 1
   @DEIN_BENUTZERNAME:DEINE_DOMAIN | admin: True | deactivated: False

Ohne Token bekommst du erwartungsgemäß eine Abfuhr:

Ausgabe
{"errcode":"M_MISSING_TOKEN","error":"Missing access token"}

Ein Endpunkt fällt allerdings aus der Reihe:

Terminal
curl -s https://DEINE_DOMAIN/_synapse/admin/v1/server_version
Ausgabe
{"server_version":"1.164.2"}

Dieser Endpunkt antwortet ohne jede Authentifizierung. Synapse macht das absichtlich, verrät damit aber jedem, welche Version du fährst und welche bekannten Lücken für dich interessant sind. Wenn du den Verwaltungspfad von außen gar nicht brauchst, sperrst du ihn am besten im Reverse Proxy: Eine IPAllowList-Middleware auf /_synapse/admin kostet zwei Zeilen und nimmt das Thema vom Tisch. Das Muster dafür steht in Dienste nur im VPN erreichbar machen.

Wenn es nicht funktioniert

Synapse startet nicht und meldet IncorrectDatabaseSetup: Database has incorrect collation. Die Datenbank wurde mit einer sprachabhängigen Sortierung angelegt, etwa de_DE.utf8; Synapse erwartet C. Das lässt sich nachträglich nicht umstellen, denn die Sortierreihenfolge steht beim Anlegen der Datenbank fest. Lösch das Volume und starte mit POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" neu. Die Fehlermeldung bietet allow_unsafe_locale als Ausweg an; nimm den nicht, denn falsch sortierte Indizes fallen dir später bei der Suche auf die Füße.

Der Föderationstester meldet FederationOK: false, obwohl die Seite im Browser lädt. Dann stimmt meist die Delegation nicht. Prüf curl -s https://DEINE_DOMAIN/.well-known/matrix/server. Die Antwort muss den Host mit Port enthalten, also etwa {"m.server":"DEINE_DOMAIN:443"}. Kommt hier ein 404, fehlt serve_server_wellknown: true in der Konfiguration oder ein anderer Dienst fängt den Pfad ab. Ebenfalls häufig: Der Servername in der Delegation zeigt auf einen Host, dessen Zertifikat auf einen anderen Namen ausgestellt ist.

In den Logs steht bei jedem Zugriff die IP von Traefik statt der des Besuchers. Es fehlt x_forwarded: true im listeners-Block. Synapse wertet die Weiterleitungs-Header nur aus, wenn du es ihm ausdrücklich erlaubst. Ohne echte Absender-IP kannst du weder Missbrauch nachvollziehen noch fail2ban sinnvoll einsetzen.

Die Anmeldung schlägt mit M_FORBIDDEN fehl, obwohl das Passwort stimmt. Prüf, ob du dich wirklich am richtigen Server anmeldest: Die Benutzer-ID lautet @name:server_name, nicht @name:host. Wenn server_name und der Host auseinanderfallen und die Client-Delegation fehlt, landet dein Programm am falschen Ort. curl -s https://DEINE_DOMAIN/.well-known/matrix/client muss die Basis-URL des Homeservers liefern.

Der Server läuft, aber der Speicher wird knapp. Synapse belegt im Leerlauf wenig (gut 100 MB), wächst aber deutlich, sobald du großen, stark föderierten Räumen beitrittst, weil er deren gesamte Historie mitverwaltet. Wenn es eng wird, hilft als Erstes, solche Räume zu verlassen und anschließend mit der Aufräumfunktion der Admin-API alte Ereignisse zu entfernen. Mehr Arbeitsspeicher ist die andere Antwort; ein Homeserver, der in echten Räumen mitspielt, ist auf 1 GB nicht glücklich.

Dein Problem ist nicht dabei? Alle Fehlermeldungen durchsuchen →

Wartung & Backups

Updates kommen häufig: Synapse veröffentlicht etwa alle zwei Wochen eine neue Version, und Sicherheitskorrekturen sind darunter. Das macht einen festen Rhythmus nötig:

Terminal
cd /opt/matrix
docker compose exec -T db pg_dump -U synapse synapse > vor-update-$(date +%F).sql
sed -i 's/synapse:v1.164.2/synapse:vNEUE_VERSION/' compose.yaml
docker compose pull && docker compose up -d
docker compose logs synapse --tail 20

Lies vor jedem Sprung die Release Notes, insbesondere den Abschnitt zu „deprecations“: Synapse wirft regelmäßig alte Konfigurationsschlüssel raus, und der Server startet dann mit einer klaren, aber unerwarteten Meldung nicht mehr. Datenbankmigrationen laufen beim Start automatisch; bei großen Instanzen kann der erste Start nach einem Update deshalb mehrere Minuten dauern.

Sichern musst du vier Dinge, und das erste ist das wichtigste:

Ausgabe
/opt/matrix/data/DEINE_DOMAIN.signing.key   # die Identität deines Servers
/opt/matrix/data/homeserver.yaml            # enthält die drei Geheimnisse
pg_dump-Ausgabe                             # Räume, Nachrichten, Konten
/opt/matrix/data/media_store/               # Bilder und Dateien aus den Räumen

Ohne den Signaturschlüssel kannst du den Server nicht wiederherstellen. Du könntest ihn nur neu aufsetzen, und alle anderen Server würden ihn als neuen, unbekannten Server behandeln. Alles zusammen gehört verschlüsselt ins Restic-Backup.

Der media_store wächst unbemerkt: Jedes Bild aus jedem föderierten Raum, den du betrittst, landet als Kopie auf deiner Platte. Schau alle paar Monate mit du -sh /opt/matrix/data/media_store nach; Synapse bringt für das Aufräumen eine Admin-API mit, die alte Medien löscht.

Realistisch gerechnet kostet das Aufsetzen einen Abend. Danach sind es zwei Termine pro Monat: einmal Update samt Blick in die Release Notes, einmal Plattenplatz und Föderationstest. Matrix ist kein Dienst, den man aufsetzt und vergisst; dafür entwickelt sich das Protokoll zu schnell.

Feedback per E-Mail: feedback@serverkueche.de

Wie geht's weiter?

Das könnte dir auch schmecken