<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Chat – Serverküche</title><link>https://serverkueche.de/tags/chat/</link><description>Chat – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 12 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/chat/index.xml" rel="self" type="application/rss+xml"/><item><title>Matrix Teil 2: Element Web, Föderation und Admin-Alltag</title><link>https://serverkueche.de/tutorials/matrix-element-federation/</link><pubDate>Thu, 12 Nov 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/matrix-element-federation/</guid><description>Element Web neben Synapse auf derselben Domain, Geräteverifizierung, verschlüsselte Räume, Einladungstokens und die Admin-API für Konten, Räume und Medien.</description><content:encoded><![CDATA[<p>Der Server aus Teil 1 läuft, föderiert und hat genau ein Konto. Nur sieht ihn niemand. Jetzt kommt
die Oberfläche dazu, und mit ihr die Fragen, die im Alltag wirklich auftauchen: Wie kommen Freunde
rein, ohne dass die Registrierung offensteht? Wer räumt die Räume auf?</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Wir stellen Element Web 1.12.33 neben Synapse 1.164.2, und zwar auf derselben Domain; Traefik teilt
die Anfragen nach Pfad auf. Danach hast du eine fertige Chat-Oberfläche im Browser, einen
Ende-zu-Ende-verschlüsselten Raum, eine Registrierung, die nur mit Einladungstoken funktioniert, und
die Admin-Handgriffe für Konten, Räume und Medien.</p>
<p>Das ist Teil 2 der Matrix-Serie. Teil 1 hat den Homeserver, die Delegation und die Föderation
aufgebaut; hier geht es um alles, was danach kommt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Warum eine Domain und nicht zwei
  </p>
  <div class="prose-kitchen text-sm">Zwei Subdomains (<code>matrix.</code> für den Server, <code>chat.</code> für Element) sind der üblichere Weg und völlig
in Ordnung. Wir machen es hier bewusst auf nur einer Domain, weil das der schwierigere Fall ist:
Sobald sich zwei Dienste einen Hostnamen teilen, entscheidet die Reihenfolge der Traefik-Regeln
darüber, wer eine Anfrage bekommt, und genau da geht es in der Praxis schief. Wer zwei Subdomains
hat, überspringt Schritt 1 und lässt in Schritt 3 die <code>priority</code>-Zeilen weg.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein laufender Matrix-Server aus
<a href="/tutorials/matrix-synapse-server/">Matrix: der eigene Chat-Server mit Synapse (Teil 1)</a>,
inklusive grünem Föderationstest</li>
<li>Debian 13 mit Docker und einem Traefik, der die Zertifikate holt</li>
<li>Die Domain aus Teil 1; der <code>server_name</code> bleibt unverändert</li>
<li>Etwa 60 Minuten, davon ein guter Teil für den ersten Login</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/matrix-element-federation/" data-content-piece="VPS 2000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 2000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCore · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 26,92 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Synapse wächst mit dem ersten großen föderierten Raum von gut 120 auf rund 280 MB. Element selbst braucht nichts – unter 5 MB.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-2000-g12.5-24m-eu?ref=44083#vps-2000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 2000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-2000" data-voucher-msg-empty="Gerade ist für diesen Tarif kein Code frei – nimm den 5,95-€-Code." data-voucher-msg-limit="Für heute sind keine weiteren Codes drin – nimm den 5,95-€-Code oder versuch es morgen." data-voucher-msg-error="Der Code ließ sich gerade nicht abrufen – nimm den 5,95-€-Code."
     data-voucher-hint="Warenkorb vorher leeren – der Code legt den Server selbst hinein.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 Monat gratis</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Zum Kopieren klicken"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Code anzeigen</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">anderer Code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Einmal einlösbar, gilt für VPS 2000 G12.5.
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 2000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">5,95 € Gutschein</span> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">immer gültig · nicht für Domains und VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/netcup-gutschein/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Alle netcup-Gutscheine →</a></p>
</div></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-wie-sich-zwei-dienste-einen-hostnamen-teilen">Schritt 1: Wie sich zwei Dienste einen Hostnamen teilen</h3>
<p>Synapse beansprucht drei Pfadpräfixe und sonst nichts:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/_matrix         # die Client- und Föderations-API
</span></span><span class="line"><span class="cl">/_synapse        # die Verwaltungs-API
</span></span><span class="line"><span class="cl">/.well-known/matrix  # die Delegation aus Teil 1</span></span></code></pre></div>
</div>
<p>Alles andere (die Startseite, JavaScript, Bilder, Schriften) gehört Element. Traefik kann das
auseinanderhalten, wenn man ihm sagt, welche Regel zuerst geprüft wird. Dafür gibt es
<code>priority</code>: Je höher die Zahl, desto früher wird die Regel ausgewertet.</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Regel                                             priority   Ergebnis
</span></span><span class="line"><span class="cl">Host(`DEINE_DOMAIN`) &amp;&amp; PathPrefix(`/_matrix`)       100      Synapse
</span></span><span class="line"><span class="cl">Host(`DEINE_DOMAIN`)                                  10      Element</span></span></code></pre></div>
</div>
<p>Ohne <code>priority</code> sortiert Traefik nach Länge der Regel, und die längere gewinnt. Das führt hier
zufällig zum richtigen Ergebnis. Verlassen kannst du dich darauf nicht, denn das kann sich mit
jeder zusätzlichen Regel ändern. Schreib die Zahlen hin.</p>
<h3 id="schritt-2-element-konfigurieren">Schritt 2: Element konfigurieren</h3>
<p>Element Web ist reines JavaScript im Browser; die gesamte Einstellung steckt in einer einzigen
<code>config.json</code>. Leg sie neben die <code>compose.yaml</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/matrix
</span></span><span class="line"><span class="cl">nano element-config.json</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">JSON</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;default_server_config&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;m.homeserver&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;base_url&#34;</span><span class="p">:</span> <span class="s2">&#34;https://DEINE_DOMAIN&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;server_name&#34;</span><span class="p">:</span> <span class="s2">&#34;DEINE_DOMAIN&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;brand&#34;</span><span class="p">:</span> <span class="s2">&#34;Kochabend Chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;disable_custom_urls&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;disable_guests&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;disable_3pid_login&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;default_country_code&#34;</span><span class="p">:</span> <span class="s2">&#34;DE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;show_labs_settings&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;room_directory&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;servers&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;DEINE_DOMAIN&#34;</span><span class="p">]</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;setting_defaults&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;breadcrumbs&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;UIFeature.feedback&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;UIFeature.registration&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Die wichtigen Schalter:</p>
<ul>
<li><code>disable_custom_urls: true</code>: Die Anmeldemaske zeigt deinen Server fest an, statt ein
Eingabefeld für beliebige Homeserver. Ohne das melden sich Leute versehentlich bei matrix.org an
und wundern sich, warum ihr Konto nicht existiert.</li>
<li><code>disable_3pid_login</code> und <code>disable_guests</code>: Kein Login über E-Mail/Telefon (dafür bräuchte
es einen Identitätsserver) und keine Gastkonten.</li>
<li><code>UIFeature.registration: false</code>: Der Link „Konto erstellen&quot; verschwindet. Die Registrierung
regeln wir in Schritt 7 über Tokens, nicht über einen offenen Knopf.</li>
<li><code>show_labs_settings: false</code>: Element hat eine ganze Schublade unfertiger Funktionen. Auf
einem Server, den andere mitbenutzen, bleibt die besser zu.</li>
</ul>
<p>Die Datei muss für den Container <strong>lesbar</strong> sein:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod <span class="m">0644</span> element-config.json</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Wenn du den Server nach Lynis gehärtet hast
  </p>
  <div class="prose-kitchen text-sm">Ein <code>umask 027</code> in <code>/etc/login.defs</code>, wie es das
<a href="/tutorials/lynis-sicherheits-audit/">Sicherheits-Audit mit Lynis</a> vorschlägt, erzeugt neue
Dateien mit <code>0640</code>. Element läuft im Container unentprivilegiert und kann die Datei dann nicht
lesen; der Container startet mit <code>cp: can't open '/app/config.json': Permission denied</code> gar nicht
erst. Das <code>chmod</code> ist deshalb Pflicht.</div>
</div>
<h3 id="schritt-3-den-stack-erweitern">Schritt 3: Den Stack erweitern</h3>
<p>In der <code>compose.yaml</code> aus Teil 1 ändern wir die Traefik-Regel von Synapse und hängen einen zweiten
Dienst an:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /opt/matrix/compose.yaml (Auszug)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">synapse</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/element-hq/synapse:v1.164.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># ... unverändert ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.rule=Host(`DEINE_DOMAIN`) &amp;&amp; (PathPrefix(`/_matrix`) || PathPrefix(`/_synapse`) || PathPrefix(`/.well-known/matrix`))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.priority=100</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.entrypoints=websecure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.tls.certresolver=le</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.services.synapse.loadbalancer.server.port=8008</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">element</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vectorim/element-web:v1.12.33</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">element</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./element-config.json:/app/config.json:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.element.rule=Host(`DEINE_DOMAIN`)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.element.priority=10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.element.entrypoints=websecure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.element.tls.certresolver=le</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.services.element.loadbalancer.server.port=80</span></span></span></code></pre></div>
</div>
<p>Element hängt nur im <code>proxy</code>-Netz. Es spricht nie mit der Datenbank und auch nicht direkt mit
Synapse: Die API-Aufrufe macht der Browser des Besuchers, nicht der Container. Das interne Netz
aus Teil 1 bleibt der Datenbank vorbehalten.</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME        STATUS
</span></span><span class="line"><span class="cl">element     Up 20 seconds (healthy)
</span></span><span class="line"><span class="cl">matrix-db   Up 20 seconds (healthy)
</span></span><span class="line"><span class="cl">synapse     Up 20 seconds (healthy)</span></span></code></pre></div>
</div>
<h3 id="schritt-4-beide-wege-gegenprüfen">Schritt 4: Beide Wege gegenprüfen</h3>
<p>Bevor du den Browser aufmachst, prüf von außen, dass die Aufteilung wirklich greift. Erst die
Wurzel, hier muss Element antworten:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/ <span class="p">|</span> grep -o <span class="s2">&#34;&lt;title&gt;[^&lt;]*&lt;/title&gt;&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&lt;title&gt;Element&lt;/title&gt;</span></span></code></pre></div>
</div>
<p>Dann die Konfiguration, die Element beim Start lädt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/config.json <span class="p">|</span> head -5</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">  &#34;default_server_config&#34;: {
</span></span><span class="line"><span class="cl">    &#34;m.homeserver&#34;: {
</span></span><span class="line"><span class="cl">      &#34;base_url&#34;: &#34;https://DEINE_DOMAIN&#34;,</span></span></code></pre></div>
</div>
<p>Und schließlich, dass Synapse seine drei Pfade behalten hat:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;%{http_code}\n&#34;</span> https://DEINE_DOMAIN/_matrix/client/versions
</span></span><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/.well-known/matrix/server
</span></span><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/.well-known/matrix/client</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">200
</span></span><span class="line"><span class="cl">{&#34;m.server&#34;:&#34;DEINE_DOMAIN:443&#34;}
</span></span><span class="line"><span class="cl">{&#34;m.homeserver&#34;:{&#34;base_url&#34;:&#34;https://DEINE_DOMAIN/&#34;}}</span></span></code></pre></div>
</div>
<p>Kommt bei einem der drei Aufrufe HTML statt JSON zurück, hat Element die Anfrage abgefangen. Dann
stimmt die <code>priority</code> nicht. Kommt beim ersten Aufruf JSON statt HTML, ist es umgekehrt.</p>
<h3 id="schritt-5-der-erste-login-und-die-geräteverifizierung">Schritt 5: Der erste Login und die Geräteverifizierung</h3>
<p>Ruf <code>https://DEINE_DOMAIN/</code> im Browser auf. Weil <code>disable_custom_urls</code> gesetzt ist, steht dein
Server fest in der Maske; du gibst nur Benutzernamen und Passwort aus Teil 1 ein.</p>
<p><figure class="my-6"><img src="/tutorials/matrix-element-federation/el-login_hu_f3945be571621a54.webp" srcset="/tutorials/matrix-element-federation/el-login_hu_8d04456c0cbd6952.webp 480w, /tutorials/matrix-element-federation/el-login_hu_f3945be571621a54.webp 768w, /tutorials/matrix-element-federation/el-login_hu_4d7a9a65f64bb09f.webp 1200w, /tutorials/matrix-element-federation/el-login_hu_77f1669e6e434e2c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matrix-element-federation/el-login_hu_6adb444e7bae3748.webp"
    alt="Die Anmeldemaske von Element Web mit fest eingetragenem Homeserver und den Feldern für Benutzername und Passwort" title="Element fragt nur noch nach Benutzername und Passwort – der Homeserver steht fest"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Element fragt nur noch nach Benutzername und Passwort – der Homeserver steht fest</figcaption></figure></p>
<p>Beim allerersten Gerät richtet Element die Verschlüsselung selbst ein, ohne Rückfrage: Es erzeugt
im Hintergrund einen Cross-Signing-Schlüssel, der deine Geräte als deine ausweist, und eine
Schlüsselsicherung auf dem Server. Der Server speichert davon nur verschlüsselte Kopien; lesen
kann er sie nicht. Zu sehen ist davon nur ein Hinweis oben links:</p>
<p><figure class="my-6"><img src="/tutorials/matrix-element-federation/el-backup-prompt_hu_9c86d22b48386748.webp" srcset="/tutorials/matrix-element-federation/el-backup-prompt_hu_a01370c5b2f88b5c.webp 480w, /tutorials/matrix-element-federation/el-backup-prompt_hu_9c86d22b48386748.webp 768w, /tutorials/matrix-element-federation/el-backup-prompt_hu_4eb6a89021b0a234.webp 1200w, /tutorials/matrix-element-federation/el-backup-prompt_hu_f26383e9fbb898b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matrix-element-federation/el-backup-prompt_hu_370248c433d82c5a.webp"
    alt="Element nach der ersten Anmeldung mit dem Hinweis Back up your chats und den Schaltflächen Dismiss und Continue" title="Der Hinweis nach dem ersten Login – hier geht es über Continue weiter, nicht über Dismiss"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der Hinweis nach dem ersten Login – hier geht es über Continue weiter, nicht über Dismiss</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Den Hinweis auf die Schlüsselsicherung nicht wegklicken
  </p>
  <div class="prose-kitchen text-sm"><p>Der Hinweis „Back up your chats&quot; sagt: „Your chats are backed up automatically. You&rsquo;ll need
your recovery key to restore them if you lose all your devices.&quot; Die Sicherung läuft also
schon, aber ohne Wiederherstellungsschlüssel kommst du nie wieder an sie heran.</p>
<p>Nimm <strong>Continue</strong>. Element öffnet die Einstellungen unter „Encryption&quot;; dort führt „Get recovery
key&quot; und „Continue&quot; zu „Save your recovery key somewhere safe&quot; mit dem Schlüssel. Der gehört in
den Passwortmanager. Wer stattdessen auf „Dismiss&quot; klickt, hat keinen.</p>
<p>Das fällt erst auf, wenn du dich auf einem weiteren Gerät anmeldest. Dann fragt Element mit
„Confirm your digital identity&quot;: „Use another device&quot; oder „Use recovery key&quot;. Ohne Schlüssel
ist die zweite Möglichkeit ausgegraut, und sind alle alten Geräte weg, bleibt nur „Can&rsquo;t
confirm?&quot;. Das setzt deine Identität zurück, und der Verlauf, der nur auf dem Server liegt, ist
nicht mehr zu entziffern. Auch nicht von dir als Serveradministrator: Ein <code>pg_dump</code> sichert die
Nachrichten, nicht die Schlüssel dazu.</p>
</div>
</div>
<h3 id="schritt-6-der-erste-verschlüsselte-raum">Schritt 6: Der erste verschlüsselte Raum</h3>
<p>Über „New room&quot; legst du einen Raum an. Zwei Einstellungen entscheiden hier über Dinge, die
später feststehen:</p>
<p><figure class="my-6"><img src="/tutorials/matrix-element-federation/el-new-room_hu_2abfc95a4a52200f.webp" srcset="/tutorials/matrix-element-federation/el-new-room_hu_a4d127e70d794188.webp 480w, /tutorials/matrix-element-federation/el-new-room_hu_2abfc95a4a52200f.webp 768w, /tutorials/matrix-element-federation/el-new-room_hu_6116fe40e65fa6b1.webp 1200w, /tutorials/matrix-element-federation/el-new-room_hu_21a85e1a2a41d70b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matrix-element-federation/el-new-room_hu_52a383807415bedd.webp"
    alt="Der Dialog Create a private room mit ausgefülltem Namen, der Auswahl Invite only und aktivierter Ende-zu-Ende-Verschlüsselung" title="Invite only lässt sich später ändern, die Verschlüsselung nicht"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Invite only lässt sich später ändern, die Verschlüsselung nicht</figcaption></figure></p>
<ul>
<li>„Invite only&quot;: Die Sichtbarkeit lässt sich später ändern.</li>
<li>„Enable end-to-end encryption&quot;: Das lässt sich <strong>nicht</strong> zurücknehmen. Element sagt es
auch dazu: „You can&rsquo;t disable this later. Bridges &amp; most bots won&rsquo;t work yet.&quot; Ein verschlüsselter Raum ist für die meisten Brücken
zu anderen Netzen und für viele Bots nicht nutzbar. Wenn du später ein Telegram- oder
IRC-Bridging planst, leg dafür einen unverschlüsselten Raum an.</li>
</ul>
<p>Danach steht der Raum, und die erste Nachricht geht raus:</p>
<p><figure class="my-6"><img src="/tutorials/matrix-element-federation/el-room_hu_e45a6970f9af2d30.webp" srcset="/tutorials/matrix-element-federation/el-room_hu_2e4e2bb49193cd00.webp 480w, /tutorials/matrix-element-federation/el-room_hu_e45a6970f9af2d30.webp 768w, /tutorials/matrix-element-federation/el-room_hu_9761714310b2da4f.webp 1200w, /tutorials/matrix-element-federation/el-room_hu_6cb18b64db806216.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matrix-element-federation/el-room_hu_a001835334524a32.webp"
    alt="Ein verschlüsselter Matrix-Raum in Element mit dem Hinweis Encryption enabled und einer ersten Nachricht in der Zeitleiste" title="Der fertige Raum: Encryption enabled steht als Systemnachricht am Anfang der Zeitleiste"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der fertige Raum: Encryption enabled steht als Systemnachricht am Anfang der Zeitleiste</figcaption></figure></p>
<p>Über die Admin-API kannst du gegenprüfen, dass der Server den Raum tatsächlich als verschlüsselt
führt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v1/rooms?limit=5&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">name: Kochabend | members: 1 | encryption: m.megolm.v1.aes-sha2</span></span></code></pre></div>
</div>
<p><code>m.megolm.v1.aes-sha2</code> ist das Gruppenverschlüsselungsverfahren von Matrix. Steht dort <code>null</code>, ist
der Raum offen lesbar, für den Server und für jeden, der die Datenbank in die Finger bekommt.</p>
<h3 id="schritt-7-freunde-einlassen-ohne-die-registrierung-zu-öffnen">Schritt 7: Freunde einlassen, ohne die Registrierung zu öffnen</h3>
<p>In Teil 1 steht <code>enable_registration: false</code>. Das ist richtig, solange du allein bist. Nur müsstest
du jedes weitere Konto von Hand anlegen und das Passwort per Chat verschicken. Der bessere Weg sind
Registrierungstokens: Die Registrierung ist offen, aber nur mit einem Einladungscode, den du
ausgibst.</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /opt/matrix/data/homeserver.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">enable_registration</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">registration_requires_token</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose restart synapse</span></span></code></pre></div>
</div>
<p>Jetzt legst du einen Token an, hier einen, der dreimal benutzt werden darf:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v1/registration_tokens/new&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;uses_allowed&#34;: 3, &#34;length&#34;: 12}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;token&#34;:&#34;LvM3cFXHXX7H&#34;,&#34;uses_allowed&#34;:3,&#34;pending&#34;:0,&#34;completed&#34;:0,&#34;expiry_time&#34;:null}</span></span></code></pre></div>
</div>
<p>Diese Zeichenfolge schickst du deinen Leuten. Element fragt sie während der Registrierung ab. Dass
ohne Token nichts geht, prüfst du am einfachsten am Anfang des Registrierungsvorgangs:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST <span class="s2">&#34;https://DEINE_DOMAIN/_matrix/client/v3/register&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> -d <span class="s1">&#39;{}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;session&#34;:&#34;PklWEdRKlnhHbBVJrwJnwnBG&#34;,
</span></span><span class="line"><span class="cl"> &#34;flows&#34;:[{&#34;stages&#34;:[&#34;m.login.registration_token&#34;,&#34;m.login.dummy&#34;]}],&#34;params&#34;:{}}</span></span></code></pre></div>
</div>
<p>Der Server verlangt jetzt zwei Stufen, und die erste ist der Token. Ohne ihn (oder mit einem
falschen, dann meldet der Server „Invalid registration token&quot;) endet der Vorgang, ohne dass ein
Konto entsteht. Nach einer erfolgreichen Registrierung siehst du den Verbrauch:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v1/registration_tokens&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;registration_tokens&#34;:[{&#34;token&#34;:&#34;LvM3cFXHXX7H&#34;,&#34;uses_allowed&#34;:3,&#34;pending&#34;:0,&#34;completed&#34;:1,&#34;expiry_time&#34;:null}]}</span></span></code></pre></div>
</div>
<p><code>completed</code> sind fertige Registrierungen, <code>pending</code> angefangene. Ist der Token aufgebraucht, läuft
er ins Leere; du musst nichts abschalten. Mit <code>&quot;expiry_time&quot;</code> (Millisekunden seit 1970) kannst du
ihm zusätzlich ein Ablaufdatum geben.</p>
<h3 id="schritt-8-föderation-im-alltag">Schritt 8: Föderation im Alltag</h3>
<p>Der Föderationstest aus Teil 1 prüft, ob andere deinen Server erreichen. Die andere Richtung,
ob dein Server rauskommt, prüfst du, indem du nach jemandem fragst, den es nur woanders gibt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://DEINE_DOMAIN/_matrix/client/v3/profile/@matthew:matrix.org&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;displayname&#34;:&#34;Matthew&#34;,&#34;avatar_url&#34;:&#34;mxc://matrix.org/oUxxDyzQOHdVDMxgwFzyCWEe&#34;}</span></span></code></pre></div>
</div>
<p>Diese Antwort kann dein Server nicht selbst erfunden haben: Er hat matrix.org gefragt, die Signatur
geprüft und das Ergebnis weitergereicht. Dasselbe funktioniert mit Raumadressen; der
Verzeichniseintrag verrät zusätzlich, welche Server im Raum sitzen.</p>
<p>Was das kostet, zeigt der Beitritt zu einem echten öffentlichen Raum:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">time</span> curl -s -X POST <span class="s2">&#34;https://DEINE_DOMAIN/_matrix/client/v3/join/%23element-web:matrix.org&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span> -H <span class="s2">&#34;Content-Type: application/json&#34;</span> -d <span class="s1">&#39;{}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;room_id&#34;:&#34;!xUW4PpAe1CmThA3r2wI8IrgwwsK006-zqWdJCljpd10&#34;}
</span></span><span class="line"><span class="cl">real    0m2.221s</span></span></code></pre></div>
</div>
<p>Zwei Sekunden. In den zwei Minuten danach, während Synapse den Raumzustand nachlädt (gut 5.000
Zustandsereignisse), steigt sein Arbeitsspeicher von rund 120 MB auf rund 280 MB, die Datenbank
von 15 auf 63 MB. Für einen einzigen Raum. Das ist der wichtigste Satz über Matrix-Föderation:
Dein Server verwaltet für jeden Raum, in dem du sitzt, dessen gesamten Zustand mit, auch den Teil,
der von fremden Servern kommt. Ein Homeserver mit 1 GB RAM ist deshalb schnell am Ende, sobald
jemand drei große Räume betritt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Raum-IDs haben ihr Format geändert
  </p>
  <div class="prose-kitchen text-sm">Die Raum-ID oben hat keinen Doppelpunkt und keinen Servernamen mehr. Ab Raumversion 12 sind
Raum-IDs reine Prüfsummen ohne Herkunftsangabe. Alte IDs der Form
<code>!abcdef:matrix.org</code> bleiben gültig. Skripte, die auf den Doppelpunkt bauen, tun es nicht.</div>
</div>
<h3 id="schritt-9-der-admin-alltag">Schritt 9: Der Admin-Alltag</h3>
<p>Drei Handgriffe brauchst du regelmäßig. <strong>Konten stilllegen.</strong> Ein Konto wird nie gelöscht, sondern
deaktiviert, weil die Benutzer-ID in fremden Räumen weiterlebt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v1/deactivate/@NAME:DEINE_DOMAIN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> -d <span class="s1">&#39;{&#34;erase&#34;: true}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;id_server_unbind_result&#34;:&#34;success&#34;}</span></span></code></pre></div>
</div>
<p><code>erase: true</code> löscht zusätzlich Anzeigename und Profilbild. Die Benutzer-ID bleibt gesperrt und kann
nicht neu vergeben werden. Das ist Absicht, sonst könnte jemand die Identität übernehmen.</p>
<p><strong>Räume loswerden</strong>, die du nicht mehr brauchst. Erst austreten und vergessen, dann vom Server
räumen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">RAUM</span><span class="o">=</span><span class="s1">&#39;!DEINE_RAUM_ID&#39;</span>
</span></span><span class="line"><span class="cl">curl -s -X DELETE <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v2/rooms/</span><span class="nv">$RAUM</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> -d <span class="s1">&#39;{&#34;purge&#34;: true, &#34;block&#34;: false}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;delete_id&#34;:&#34;qUcHmFcjfVZFfHWv&#34;}</span></span></code></pre></div>
</div>
<p>Der Aufruf kommt sofort zurück, das Aufräumen läuft im Hintergrund. Den Stand fragst du unter
<code>/_synapse/admin/v2/rooms/delete_status/DELETE_ID</code> ab, bis dort <code>&quot;status&quot;:&quot;complete&quot;</code> steht. Mit
<code>&quot;block&quot;: true</code> sperrst du den Raum zusätzlich, sodass niemand ihn erneut betreten kann.</p>
<p><strong>Medien aufräumen.</strong> Jedes Bild aus jedem föderierten Raum landet als Kopie bei dir:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">VORHER</span><span class="o">=</span><span class="k">$((</span> <span class="o">(</span><span class="k">$(</span>date +%s<span class="k">)</span> <span class="o">-</span> <span class="m">2592000</span><span class="o">)</span> <span class="o">*</span> <span class="m">1000</span> <span class="k">))</span>
</span></span><span class="line"><span class="cl">curl -s -X POST <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v1/media/DEINE_DOMAIN/delete?before_ts=</span><span class="nv">$VORHER</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;deleted_media&#34;:[],&#34;total&#34;:0}</span></span></code></pre></div>
</div>
<p><code>before_ts</code> ist ein Zeitstempel in <strong>Millisekunden</strong>; die Rechnung oben ergibt „älter als 30 Tage&quot;.
Vergisst man die drei Nullen, löscht der Aufruf nichts, weil der Zeitpunkt 1970 liegt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Die Admin-API gehört nicht ins offene Netz
  </p>
  <div class="prose-kitchen text-sm">Alle Aufrufe in diesem Abschnitt hängen an einem einzigen Token. Wer ihn hat, kann Konten
deaktivieren und Räume löschen. Sperr <code>/_synapse/admin</code> im Reverse Proxy auf dein VPN oder deine
feste Adresse; das Muster dafür steht in
<a href="/tutorials/interne-dienste-nur-im-vpn/">Dienste nur im VPN erreichbar machen</a>.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Unter <code>/_matrix/client/versions</code> kommt die Element-Startseite statt JSON.</strong> Beide Router hören auf denselben Host, und Traefik hat den falschen zuerst geprüft. Setz <code>traefik.http.routers.synapse.priority=100</code> und <code>…element.priority=10</code>. Ohne ausdrückliche Zahlen sortiert Traefik nach Regellänge, und das Ergebnis ändert sich, sobald eine weitere Regel dazukommt. Nach dem Ändern reicht <code>docker compose up -d</code>; Traefik liest die Labels sofort neu.</p>
<p><strong>Der Element-Container startet nicht und meldet <code>cp: can't open '/app/config.json': Permission denied</code>.</strong> Die <code>element-config.json</code> auf dem Host ist für den Container nicht lesbar. Das passiert zuverlässig auf Servern mit <code>umask 027</code>, weil neue Dateien dann mit <code>0640</code> und ohne Leserecht für andere entstehen. <code>chmod 0644 /opt/matrix/element-config.json</code> und neu starten. Das gilt für jede Konfigurationsdatei, die du per Bind-Mount in einen Container reichst.</p>
<p><strong>Element hängt bei „Confirm your digital identity&quot;, und „Use recovery key&quot; ist ausgegraut.</strong> Du meldest dich auf einem weiteren Gerät an und hast beim ersten keinen Wiederherstellungsschlüssel angelegt (Schritt 5). Ist ein altes Gerät noch angemeldet, nimm „Use another device&quot; und bestätige dort. Sonst bleibt nur „Can&rsquo;t confirm?&quot; mit anschließendem Kontopasswort: Element setzt die Identität dann zurück, Konto, Kontakte und Räume bleiben, aber Nachrichten aus der Zeit davor bleiben unlesbar. Leg danach sofort den Wiederherstellungsschlüssel an.</p>
<p><strong>Die Anmeldung schlägt plötzlich mit <code>M_LIMIT_EXCEEDED</code> fehl.</strong> In der Antwort steht <code>retry_after_ms</code>, die Wartezeit in Millisekunden. Synapse bremst Anmeldeversuche pro Konto und pro IP ab; das trifft dich, wenn du mehrere Sitzungen hintereinander aufbaust oder mit Skripten testest. Für den Dauerbetrieb lässt sich das über den <code>rc_login</code>-Block in der <code>homeserver.yaml</code> lockern, nur solltest du das nicht tun, denn genau diese Bremse hält Rateversuche auf.</p>
<p><strong>In der Geräteliste und in den Protokollen steht <code>172.x.x.x</code> statt der echten Besucher-IP.</strong> Das betrifft nur Anfragen über <strong>IPv6</strong>, bei einer Domain mit AAAA-Eintrag also die Mehrheit. Das <code>proxy</code>-Netz wird im Traefik-Tutorial mit <code>docker network create proxy</code> angelegt, und das ist ein reines IPv4-Netz. Docker kann eine eingehende IPv6-Verbindung dann nicht direkt auf den Container umschreiben und schickt sie über einen Hilfsprozess, der die Absenderadresse durch die des Bridge-Gateways ersetzt; über IPv4 stimmt sie die ganze Zeit. Abhilfe ist ein Netz mit IPv6: in allen Stacks <code>docker compose down</code>, dann <code>docker network rm proxy &amp;&amp; docker network create --ipv6 --subnet fd00:cafe::/64 proxy</code> und alles wieder hochfahren; die Schritte stehen im <a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a>. Danach steht bei IPv4 <strong>und</strong> IPv6 die echte Adresse in <code>X-Forwarded-For</code>.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p>Element und Synapse aktualisierst du getrennt. Element Web ist eine Sammlung statischer Dateien
ohne eigenen Zustand; ein Update ist risikoarm und kostet Sekunden:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/matrix
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/element-web:v1.12.33/element-web:vNEUE_VERSION/&#39;</span> compose.yaml
</span></span><span class="line"><span class="cl">docker compose pull element <span class="o">&amp;&amp;</span> docker compose up -d element
</span></span><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/version</span></span></code></pre></div>
</div>
<p>Der letzte Aufruf gibt die laufende Version aus, etwa <code>1.12.33</code>. Element folgt Synapse locker;
ein Versionssprung des Servers macht die Oberfläche nicht kaputt. Umgekehrt gilt das ebenfalls,
solange du nicht mehrere Jahre überspringst.</p>
<p>Was zusätzlich ins Backup muss, über Teil 1 hinaus:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/opt/matrix/element-config.json   # steckt in keiner Datenbank</span></span></code></pre></div>
</div>
<p>Das ist eine kleine Datei, aber sie enthält jede Entscheidung aus Schritt 2. Geht sie verloren,
startet Element mit der Standardkonfiguration, und deine Leute stehen plötzlich vor einem
Eingabefeld, in das sie irgendeinen Homeserver tippen können.</p>
<p>Registrierungstokens brauchen Hygiene. Ein Token mit <code>uses_allowed: 3</code> ohne Ablaufdatum ist ein
Einladungscode, der jahrelang gültig bleibt. Schau alle paar Monate in die Liste und lösch, was du
nicht mehr brauchst:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X DELETE <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v1/registration_tokens/DER_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<p>Der Speicherbedarf ist die eigentliche Wartungsaufgabe. Der Sprung von rund 120 auf 280 MB
für einen einzigen föderierten Raum skaliert weiter. Setz dir eine Erinnerung, alle paar Monate
<code>docker stats --no-stream</code> und <code>du -sh /opt/matrix/data/media_store</code> anzuschauen, und tritt aus
Räumen aus, in denen niemand von euch mehr schreibt. Das ist wirksamer als jede Feineinstellung.</p>
<p>Realistisch gerechnet macht Element selbst keine Arbeit: zwei, drei Updates im Jahr. Die Zeit
geht in den Homeserver: alle zwei Wochen ein Synapse-Update mit Blick in die Release Notes, einmal
im Quartal Medien und Räume aufräumen. Zusammen etwa eine Stunde im Monat.</p>
]]></content:encoded></item><item><title>Matrix: der eigene Chat-Server mit Synapse (Teil 1)</title><link>https://serverkueche.de/tutorials/matrix-synapse-server/</link><pubDate>Tue, 10 Nov 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/matrix-synapse-server/</guid><description>Synapse 1.164 mit Postgres 18 hinter Traefik: Server-Name, Delegation, geschlossene Registrierung – und der Föderationstest, der am Ende grün sein muss.</description><content:encoded><![CDATA[<p>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.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>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.</p>
<p>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.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Eine Entscheidung, die bleibt
  </p>
  <div class="prose-kitchen text-sm">Der <code>server_name</code> wird Teil jeder Benutzer-ID (<code>@du:DEINE_DOMAIN</code>) und jeder Raumadresse
(<code>#kochabend:DEINE_DOMAIN</code>). 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.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><a href="/tutorials/reverse-proxy-traefik/">Traefik als Reverse Proxy</a> mit funktionierendem
Let&rsquo;s-Encrypt-Resolver</li>
<li>Eine <a href="/tutorials/domain-mit-server-verbinden/">Domain, die auf den Server zeigt</a> (A- und
idealerweise AAAA-Record)</li>
<li>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</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/matrix-synapse-server/" data-content-piece="VPS 2000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 2000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCore · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 26,92 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Matrix wächst mit den Räumen, denen du beitrittst – der größere VPS ist hier keine Verschwendung.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-2000-g12.5-24m-eu?ref=44083#vps-2000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 2000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-2000" data-voucher-msg-empty="Gerade ist für diesen Tarif kein Code frei – nimm den 5,95-€-Code." data-voucher-msg-limit="Für heute sind keine weiteren Codes drin – nimm den 5,95-€-Code oder versuch es morgen." data-voucher-msg-error="Der Code ließ sich gerade nicht abrufen – nimm den 5,95-€-Code."
     data-voucher-hint="Warenkorb vorher leeren – der Code legt den Server selbst hinein.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 Monat gratis</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Zum Kopieren klicken"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Code anzeigen</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">anderer Code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Einmal einlösbar, gilt für VPS 2000 G12.5.
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 2000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">5,95 € Gutschein</span> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">immer gültig · nicht für Domains und VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/netcup-gutschein/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Alle netcup-Gutscheine →</a></p>
</div></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-server-name-host-und-delegation-auseinanderhalten">Schritt 1: Server-Name, Host und Delegation auseinanderhalten</h3>
<p>Matrix kennt zwei verschiedene Adressen, und das ist die häufigste Verwirrung am Anfang:</p>
<ul>
<li>Der <code>server_name</code> steht in den IDs: <code>@koch:DEINE_DOMAIN</code>. Er ist reine Namensgebung.</li>
<li>Der Host, auf dem Synapse tatsächlich läuft, kann ein ganz anderer sein, etwa
<code>matrix.DEINE_DOMAIN</code>.</li>
</ul>
<p>Damit fremde Server den zweiten finden, wenn sie nur den ersten kennen, gibt es <strong>Delegation</strong>:
Unter <code>https://DEINE_DOMAIN/.well-known/matrix/server</code> liegt eine kleine JSON-Datei, die auf den
echten Host zeigt. Genauso findet ein Chat-Programm über
<code>https://DEINE_DOMAIN/.well-known/matrix/client</code>, wohin es sich verbinden soll.</p>
<p>In dieser Anleitung laufen <code>server_name</code> 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 <code>matrix.DEINE_DOMAIN</code> umziehen, bleibt der <code>server_name</code> gleich und
nur die Delegationsdatei ändert sich.</p>
<h3 id="schritt-2-die-konfiguration-erzeugen-lassen">Schritt 2: Die Konfiguration erzeugen lassen</h3>
<p>Synapse schreibt sich seine Grundkonfiguration selbst, inklusive der Geheimnisse und des
Signaturschlüssels:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/matrix/data <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/matrix
</span></span><span class="line"><span class="cl">docker run --rm -v /opt/matrix/data:/data <span class="se">\
</span></span></span><span class="line"><span class="cl">  -e <span class="nv">SYNAPSE_SERVER_NAME</span><span class="o">=</span>DEINE_DOMAIN <span class="se">\
</span></span></span><span class="line"><span class="cl">  -e <span class="nv">SYNAPSE_REPORT_STATS</span><span class="o">=</span>no <span class="se">\
</span></span></span><span class="line"><span class="cl">  ghcr.io/element-hq/synapse:v1.164.2 generate</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Creating log config /data/DEINE_DOMAIN.log.config
</span></span><span class="line"><span class="cl">Setting ownership on /data to 991:991
</span></span><span class="line"><span class="cl">Generating config file /data/homeserver.yaml
</span></span><span class="line"><span class="cl">Generating signing key file /data/DEINE_DOMAIN.signing.key
</span></span><span class="line"><span class="cl">A config file has been generated in &#39;/data/homeserver.yaml&#39; for server name &#39;DEINE_DOMAIN&#39;. Please review this file and customise it to your needs.</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Der Signaturschlüssel ist unersetzlich
  </p>
  <div class="prose-kitchen text-sm">Die Datei <code>DEINE_DOMAIN.signing.key</code> 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 <code>registration_shared_secret</code>, <code>macaroon_secret_key</code> und <code>form_secret</code> aus
der <code>homeserver.yaml</code>.</div>
</div>
<p>Die erzeugte Datei benutzt SQLite. Für alles außer einem Einzelplatz-Test ist das zu wenig; wir
stellen in der <code>homeserver.yaml</code> auf PostgreSQL um und ergänzen die Einstellungen, die diesen Server
betreffen:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /opt/matrix/data/homeserver.yaml (Auszug – Geheimnisse bleiben unangetastet)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">listeners</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8008</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tls</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">x_forwarded</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">bind_addresses</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;0.0.0.0&#39;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">names</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">client, federation]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">compress</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">database</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">psycopg2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="l">synapse</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">DEIN_DB_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dbname</span><span class="p">:</span><span class="w"> </span><span class="l">synapse</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">5432</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cp_min</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cp_max</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">public_baseurl</span><span class="p">:</span><span class="w"> </span><span class="l">https://DEINE_DOMAIN/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">serve_server_wellknown</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">enable_registration</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">suppress_key_server_warning</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">max_upload_size</span><span class="p">:</span><span class="w"> </span><span class="l">50M</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">url_preview_enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span></span></span></code></pre></div>
</div>
<p>Vier Zeilen verdienen eine Erklärung:</p>
<ul>
<li><code>x_forwarded: true</code>: Synapse liest die echte Besucher-IP aus dem <code>X-Forwarded-For</code>-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.</li>
<li><code>public_baseurl</code>: Daraus baut Synapse die Client-Delegation und alle Links in Einladungen.</li>
<li><code>serve_server_wellknown: true</code>: Synapse liefert <code>/.well-known/matrix/server</code> selbst aus. Das
ist der Grund, warum wir keinen zweiten Webserver brauchen.</li>
<li><code>url_preview_enabled: false</code>: 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.</li>
</ul>
<h3 id="schritt-3-postgresql-mit-der-richtigen-sortierung">Schritt 3: PostgreSQL mit der richtigen Sortierung</h3>
<p>Synapse verlangt eine Datenbank mit der Sortierreihenfolge <code>C</code>. Der Grund ist unspektakulär und
die Folge unangenehm: Mit einer sprachabhängigen Sortierung wie <code>de_DE.utf8</code> vergleicht Postgres
Zeichenketten anders, und Synapse verweigert deshalb von vornherein den Start.</p>
<p>Im Docker-Image erreichst du das über <code>POSTGRES_INITDB_ARGS</code> – <strong>beim allerersten Start</strong>, denn
danach steht die Sortierreihenfolge fest:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_INITDB_ARGS</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;--encoding=UTF8 --lc-collate=C --lc-ctype=C&#34;</span></span></span></code></pre></div>
</div>
<p>Nachsehen kannst du das jederzeit:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> db psql -U synapse -d synapse <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c <span class="s2">&#34;SELECT datname, pg_encoding_to_char(encoding), datcollate FROM pg_database WHERE datname=&#39;synapse&#39;;&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"> datname | pg_encoding_to_char | datcollate
</span></span><span class="line"><span class="cl">---------+---------------------+------------
</span></span><span class="line"><span class="cl"> synapse | UTF8                | C</span></span></code></pre></div>
</div>
<h3 id="schritt-4-der-stack">Schritt 4: Der Stack</h3>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /opt/matrix/compose.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:18.6-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">matrix-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">synapse</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${POSTGRES_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">synapse</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_INITDB_ARGS</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;--encoding=UTF8 --lc-collate=C --lc-ctype=C&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">pgdata:/var/lib/postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">matrix-intern]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD-SHELL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;pg_isready -U synapse&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">synapse</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/element-hq/synapse:v1.164.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">synapse</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SYNAPSE_CONFIG_PATH</span><span class="p">:</span><span class="w"> </span><span class="l">/data/homeserver.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">TZ</span><span class="p">:</span><span class="w"> </span><span class="l">Europe/Berlin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">matrix-intern, proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.rule=Host(`DEINE_DOMAIN`)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.entrypoints=websecure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.synapse.tls.certresolver=le</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.services.synapse.loadbalancer.server.port=8008</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pgdata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">matrix-intern</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">internal</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Die Datenbank liegt in einem Netz mit <code>internal: true</code> – von außen und aus dem <code>proxy</code>-Netz nicht
erreichbar. Das Datenbankpasswort steht in einer <code>.env</code> daneben und muss mit dem Wert in der
<code>homeserver.yaml</code> übereinstimmen.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Hinweis
  </p>
  <div class="prose-kitchen text-sm">Der Mount der Datenbank liegt auf <code>/var/lib/postgresql</code> und nicht auf <code>/var/lib/postgresql/data</code>.
Ab PostgreSQL 18 verweigert der Container mit dem alten Pfad den Start, auch bei leerem Volume.</div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME        STATUS
</span></span><span class="line"><span class="cl">matrix-db   Up 55 seconds (healthy)
</span></span><span class="line"><span class="cl">synapse     Up 45 seconds (healthy)</span></span></code></pre></div>
</div>
<p>Beim ersten Start legt Synapse das Schema an und fährt anschließend Hintergrund-Migrationen.
In den Logs stehen dann minutenlang Zeilen wie
<code>Running background update 'threads_backfill'</code>. Das ist normal und stört den Betrieb nicht.</p>
<h3 id="schritt-5-die-vier-endpunkte-die-stimmen-müssen">Schritt 5: Die vier Endpunkte, die stimmen müssen</h3>
<p>Bevor du irgendein Chat-Programm anfasst, prüfst du von außen vier Adressen. Zuerst, ob die
Client-API überhaupt antwortet:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/_matrix/client/versions</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;versions&#34;:[&#34;r0.0.1&#34;,&#34;r0.1.0&#34;, … ,&#34;v1.16&#34;,&#34;v1.17&#34;],&#34;unstable_features&#34;:{ … }}</span></span></code></pre></div>
</div>
<p>Dann die beiden Delegationsdateien, die Synapse dank <code>serve_server_wellknown</code> selbst ausliefert:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/.well-known/matrix/server
</span></span><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/.well-known/matrix/client</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;m.server&#34;:&#34;DEINE_DOMAIN:443&#34;}
</span></span><span class="line"><span class="cl">{&#34;m.homeserver&#34;:{&#34;base_url&#34;:&#34;https://DEINE_DOMAIN/&#34;}}</span></span></code></pre></div>
</div>
<p>Und schließlich der Schlüsselendpunkt, über den fremde Server die Signaturen deines Servers prüfen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/_matrix/key/v2/server</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;old_verify_keys&#34;:{},&#34;server_name&#34;:&#34;DEINE_DOMAIN&#34;,&#34;signatures&#34;:{&#34;DEINE_DOMAIN&#34;:{&#34;ed25519:a_udcw&#34;:&#34; … &#34;}}, … }</span></span></code></pre></div>
</div>
<p>Zum Schluss die Kontrolle, dass die Tür zu ist:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST https://DEINE_DOMAIN/_matrix/client/v3/register -d <span class="s1">&#39;{}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;errcode&#34;:&#34;M_FORBIDDEN&#34;,&#34;error&#34;:&#34;Registration has been disabled&#34;}</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Offene Registrierung ist keine Option
  </p>
  <div class="prose-kitchen text-sm">Ein Matrix-Server mit offener Registrierung wird zuverlässig innerhalb von Tagen für Spam
missbraucht. Dann landet dein <code>server_name</code> auf Sperrlisten anderer Server, und das bekommst du nur
schwer wieder los. <code>enable_registration: false</code> bleibt, und Konten legst du von Hand an.</div>
</div>
<h3 id="schritt-6-das-erste-konto-anlegen">Schritt 6: Das erste Konto anlegen</h3>
<p>Weil die Registrierung zu ist, benutzt du das mitgelieferte Werkzeug. Es authentifiziert sich über
das <code>registration_shared_secret</code> aus der Konfiguration, <code>-a</code> macht den Benutzer zum Administrator:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> synapse register_new_matrix_user <span class="se">\
</span></span></span><span class="line"><span class="cl">  -u DEIN_BENUTZERNAME -p DEIN_PASSWORT -a <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c /data/homeserver.yaml http://localhost:8008</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Sending registration request...
</span></span><span class="line"><span class="cl">Success!</span></span></code></pre></div>
</div>
<h3 id="schritt-7-gegenprobe-über-die-client-api">Schritt 7: Gegenprobe über die Client-API</h3>
<p>Jetzt der eigentliche Funktionstest, ohne Chat-Programm und nur mit <code>curl</code>. Erst anmelden:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST https://DEINE_DOMAIN/_matrix/client/v3/login <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;type&#34;:&#34;m.login.password&#34;,&#34;identifier&#34;:{&#34;type&#34;:&#34;m.id.user&#34;,&#34;user&#34;:&#34;DEIN_BENUTZERNAME&#34;},&#34;password&#34;:&#34;DEIN_PASSWORT&#34;}&#39;</span></span></span></code></pre></div>
</div>
<p>Die Antwort enthält <code>user_id</code>, <code>device_id</code> und einen <code>access_token</code>. Die Benutzer-ID zeigt, dass der
<code>server_name</code> sitzt:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&#34;user_id&#34;:&#34;@DEIN_BENUTZERNAME:DEINE_DOMAIN&#34;</span></span></code></pre></div>
</div>
<p>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:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST https://DEINE_DOMAIN/_matrix/client/v3/createRoom <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span> -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;name&#34;:&#34;Kochabend&#34;,&#34;preset&#34;:&#34;private_chat&#34;}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;room_id&#34;:&#34;!68AeABE_Q59hjgJkh8wIl3zdGbp_n7k4a9uOf4yDrF0&#34;}</span></span></code></pre></div>
</div>
<p>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.</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X PUT <span class="s2">&#34;https://DEINE_DOMAIN/_matrix/client/v3/rooms/DEINE_RAUM_ID/send/m.room.message/txn1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span> -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;msgtype&#34;:&#34;m.text&#34;,&#34;body&#34;:&#34;Zwiebelkuchen um 19:15 Uhr&#34;}&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;event_id&#34;:&#34;$WHceSNXyP34qT0CoQjq0E97dm7p7ZVLArH7mZdKftyA&#34;}</span></span></code></pre></div>
</div>
<h3 id="schritt-8-föderation-prüfen">Schritt 8: Föderation prüfen</h3>
<p>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:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://federationtester.matrix.org/api/report?server_name=DEINE_DOMAIN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import sys,json; d=json.load(sys.stdin); print(&#39;FederationOK:&#39;, d[&#39;FederationOK&#39;])&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FederationOK: True</span></span></code></pre></div>
</div>
<p>Der Prüfer führt beide Adressen (IPv4 und IPv6) einzeln auf; bei beiden muss
<code>ValidCertificates: True</code> und <code>MatchingServerName: True</code> stehen. Genau das willst du sehen, bevor du jemandem deine Matrix-Adresse gibst.
Dieselbe Prüfung gibt es auch als Weboberfläche unter
<a href="https://federationtester.matrix.org/">federationtester.matrix.org</a>; die API eignet sich besser, um
sie später gelegentlich zu wiederholen.</p>
<h3 id="schritt-9-die-admin-api-abschotten">Schritt 9: Die Admin-API abschotten</h3>
<p>Synapse bringt eine eigene Verwaltungs-API mit, die unter <code>/_synapse/admin/</code> liegt. Mit dem Token
deines Administratorkontos siehst du damit zum Beispiel alle Konten:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://DEINE_DOMAIN/_synapse/admin/v2/users?from=0&amp;limit=5&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer DEIN_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">total: 1
</span></span><span class="line"><span class="cl">   @DEIN_BENUTZERNAME:DEINE_DOMAIN | admin: True | deactivated: False</span></span></code></pre></div>
</div>
<p>Ohne Token bekommst du erwartungsgemäß eine Abfuhr:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;errcode&#34;:&#34;M_MISSING_TOKEN&#34;,&#34;error&#34;:&#34;Missing access token&#34;}</span></span></code></pre></div>
</div>
<p>Ein Endpunkt fällt allerdings aus der Reihe:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/_synapse/admin/v1/server_version</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;server_version&#34;:&#34;1.164.2&#34;}</span></span></code></pre></div>
</div>
<p>Dieser Endpunkt antwortet <strong>ohne jede Authentifizierung</strong>. 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 <code>IPAllowList</code>-Middleware auf <code>/_synapse/admin</code> kostet zwei Zeilen und nimmt das Thema vom
Tisch. Das Muster dafür steht in
<a href="/tutorials/interne-dienste-nur-im-vpn/">Dienste nur im VPN erreichbar machen</a>.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Synapse startet nicht und meldet <code>IncorrectDatabaseSetup: Database has incorrect collation</code>.</strong> Die Datenbank wurde mit einer sprachabhängigen Sortierung angelegt, etwa <code>de_DE.utf8</code>; Synapse erwartet <code>C</code>. Das lässt sich nachträglich nicht umstellen, denn die Sortierreihenfolge steht beim Anlegen der Datenbank fest. Lösch das Volume und starte mit <code>POSTGRES_INITDB_ARGS: &quot;--encoding=UTF8 --lc-collate=C --lc-ctype=C&quot;</code> neu. Die Fehlermeldung bietet <code>allow_unsafe_locale</code> als Ausweg an; nimm den nicht, denn falsch sortierte Indizes fallen dir später bei der Suche auf die Füße.</p>
<p><strong>Der Föderationstester meldet <code>FederationOK: false</code>, obwohl die Seite im Browser lädt.</strong> Dann stimmt meist die Delegation nicht. Prüf <code>curl -s https://DEINE_DOMAIN/.well-known/matrix/server</code>. Die Antwort muss den Host mit Port enthalten, also etwa <code>{&quot;m.server&quot;:&quot;DEINE_DOMAIN:443&quot;}</code>. Kommt hier ein 404, fehlt <code>serve_server_wellknown: true</code> 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.</p>
<p><strong>In den Logs steht bei jedem Zugriff die IP von Traefik statt der des Besuchers.</strong> Es fehlt <code>x_forwarded: true</code> im <code>listeners</code>-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.</p>
<p><strong>Die Anmeldung schlägt mit <code>M_FORBIDDEN</code> fehl, obwohl das Passwort stimmt.</strong> Prüf, ob du dich wirklich am richtigen Server anmeldest: Die Benutzer-ID lautet <code>@name:server_name</code>, nicht <code>@name:host</code>. Wenn <code>server_name</code> und der Host auseinanderfallen und die Client-Delegation fehlt, landet dein Programm am falschen Ort. <code>curl -s https://DEINE_DOMAIN/.well-known/matrix/client</code> muss die Basis-URL des Homeservers liefern.</p>
<p><strong>Der Server läuft, aber der Speicher wird knapp.</strong> 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.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p>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:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/matrix
</span></span><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -T db pg_dump -U synapse synapse &gt; vor-update-<span class="k">$(</span>date +%F<span class="k">)</span>.sql
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/synapse:v1.164.2/synapse:vNEUE_VERSION/&#39;</span> compose.yaml
</span></span><span class="line"><span class="cl">docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs synapse --tail <span class="m">20</span></span></span></code></pre></div>
</div>
<p>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.</p>
<p>Sichern musst du vier Dinge, und das erste ist das wichtigste:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/opt/matrix/data/DEINE_DOMAIN.signing.key   # die Identität deines Servers
</span></span><span class="line"><span class="cl">/opt/matrix/data/homeserver.yaml            # enthält die drei Geheimnisse
</span></span><span class="line"><span class="cl">pg_dump-Ausgabe                             # Räume, Nachrichten, Konten
</span></span><span class="line"><span class="cl">/opt/matrix/data/media_store/               # Bilder und Dateien aus den Räumen</span></span></code></pre></div>
</div>
<p>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 <a href="/tutorials/backups-mit-restic/">Restic-Backup</a>.</p>
<p>Der <code>media_store</code> wächst unbemerkt: Jedes Bild aus jedem föderierten Raum, den du betrittst,
landet als Kopie auf deiner Platte. Schau alle paar Monate mit <code>du -sh /opt/matrix/data/media_store</code>
nach; Synapse bringt für das Aufräumen eine Admin-API mit, die alte Medien löscht.</p>
<p>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.</p>
]]></content:encoded></item></channel></rss>