<?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>Element – Serverküche</title><link>https://serverkueche.de/en/tags/element/</link><description>Element – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</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/en/tags/element/index.xml" rel="self" type="application/rss+xml"/><item><title>Matrix Part 2: Element Web, Federation and Day-to-Day Admin</title><link>https://serverkueche.de/en/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/en/tutorials/matrix-element-federation/</guid><description>Element Web next to Synapse on the same domain, device verification, encrypted rooms, invite tokens and the admin API for accounts, rooms and media.</description><content:encoded><![CDATA[<p>The server from part 1 runs, federates and has exactly one account. Only nobody can see it. Now the
interface joins in, and with it the questions that actually come up in daily use: how do friends get
in without registration standing wide open? Who cleans up the rooms?</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We put Element Web 1.12.33 next to Synapse 1.164.2, on the same domain; Traefik splits the
requests by path. Afterwards you have a finished chat interface in the browser, an end-to-end
encrypted room, a registration that only works with an invite token, and the admin moves for
accounts, rooms and media.</p>
<p>This is part 2 of the Matrix series. Part 1 built the homeserver, the delegation and federation;
this one is about everything that comes after.</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>Why one domain and not two
  </p>
  <div class="prose-kitchen text-sm">Two subdomains (<code>matrix.</code> for the server, <code>chat.</code> for Element) are the more common route and
perfectly fine. We deliberately do it on just one domain here because that is the harder case: as
soon as two services share a hostname, the order of the Traefik rules decides who gets a request,
and that is exactly where it goes wrong in practice. If you have two subdomains, skip step 1 and
leave out the <code>priority</code> lines in step 3.</div>
</div>
<h2 id="requirements">Requirements</h2>
<ul>
<li>A running Matrix server from
<a href="/en/tutorials/matrix-synapse-server/">Matrix: Your Own Chat Server with Synapse (Part 1)</a>,
including a green federation test</li>
<li>Debian 13 with Docker and a Traefik that fetches the certificates</li>
<li>The domain from part 1; the <code>server_name</code> stays unchanged</li>
<li>About 60 minutes, a good part of it for the first 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 · /en/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>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</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 vCores · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €26.92/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Synapse grows from a good 120 to around 280 MB with the first large federated room. Element itself needs nothing – under 5 MB.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-2000-g12.5-iv-24m-eu?ref=44083#vps-2000-g12.5-iv-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">
  Go to 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="No code is available for this plan right now – use the €5.95 code." data-voucher-msg-limit="No more codes today – use the €5.95 code or try again tomorrow." data-voucher-msg-error="The code couldn&#39;t be fetched right now – use the €5.95 code."
     data-voucher-hint="Empty the cart first – the code adds the server itself.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 month free</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Click to copy"
            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">Show code</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">another code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Single use, valid for VPS 2000 G12.5.
    <a href="https://www.netcup.com/en/checkout/cart?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">Redeem in the cart →</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 voucher</span> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" 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">always valid · not for domains or VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/en/netcup-voucher/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">All netcup vouchers →</a></p>
</div></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-how-two-services-share-one-hostname">Step 1: How two services share one hostname</h3>
<p>Synapse lays claim to three path prefixes and nothing else:</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         # the client and federation API
</span></span><span class="line"><span class="cl">/_synapse        # the admin API
</span></span><span class="line"><span class="cl">/.well-known/matrix  # the delegation from part 1</span></span></code></pre></div>
</div>
<p>Everything else (the start page, JavaScript, images, fonts) belongs to Element. Traefik can tell
them apart if you tell it which rule is checked first. That is what <code>priority</code> is for: the
higher the number, the earlier the rule is evaluated.</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">Rule                                              priority   Result
</span></span><span class="line"><span class="cl">Host(`YOUR_DOMAIN`) &amp;&amp; PathPrefix(`/_matrix`)        100      Synapse
</span></span><span class="line"><span class="cl">Host(`YOUR_DOMAIN`)                                   10      Element</span></span></code></pre></div>
</div>
<p>Without <code>priority</code>, Traefik sorts by rule length and the longer one wins. That happens to give the
right result here, but you cannot rely on it: it can change with every rule you add. Write the numbers
down.</p>
<h3 id="step-2-configure-element">Step 2: Configure Element</h3>
<p>Element Web is pure JavaScript in the browser; the entire configuration sits in a single
<code>config.json</code>. Put it next to the <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://YOUR_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;YOUR_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;Kitchen 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;YOUR_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>The switches that matter:</p>
<ul>
<li><code>disable_custom_urls: true</code>: The sign-in mask shows your server fixed instead of an input
field for arbitrary homeservers. Without it people accidentally sign in to matrix.org and wonder
why their account does not exist.</li>
<li><code>disable_3pid_login</code> and <code>disable_guests</code>: No login via email/phone (that would need an
identity server) and no guest accounts.</li>
<li><code>UIFeature.registration: false</code>: The &ldquo;Create account&rdquo; link disappears. We handle registration
in step 7 via tokens, not via an open button.</li>
<li><code>show_labs_settings: false</code>: Element has a whole drawer of unfinished features. On a server
other people share, that drawer is better left shut.</li>
</ul>
<p>The file has to be <strong>readable</strong> for the container:</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>If you hardened the server after Lynis
  </p>
  <div class="prose-kitchen text-sm">A <code>umask 027</code> in <code>/etc/login.defs</code>, as suggested by the
<a href="/en/tutorials/lynis-security-audit/">security audit with Lynis</a>, creates new files with <code>0640</code>.
Element runs unprivileged inside the container and then cannot read the file; the container refuses
to start with <code>cp: can't open '/app/config.json': Permission denied</code>. The <code>chmod</code> is therefore
mandatory.</div>
</div>
<h3 id="step-3-extend-the-stack">Step 3: Extend the stack</h3>
<p>In the <code>compose.yaml</code> from part 1 we change Synapse&rsquo;s Traefik rule and attach a second service:</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 (excerpt)</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"># ... unchanged ...</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(`YOUR_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(`YOUR_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 sits only in the <code>proxy</code> network. It never talks to the database and not directly to
Synapse either: the visitor&rsquo;s browser makes the API calls, not the container. The internal network
from part 1 stays reserved for the database.</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="step-4-counter-check-both-routes">Step 4: Counter-check both routes</h3>
<p>Before you open a browser, check from the outside that the split really works. First the root, where
Element has to answer:</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://YOUR_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>Then the configuration Element loads at startup:</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://YOUR_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://YOUR_DOMAIN&#34;,</span></span></code></pre></div>
</div>
<p>And finally that Synapse kept its three paths:</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://YOUR_DOMAIN/_matrix/client/versions
</span></span><span class="line"><span class="cl">curl -s https://YOUR_DOMAIN/.well-known/matrix/server
</span></span><span class="line"><span class="cl">curl -s https://YOUR_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;YOUR_DOMAIN:443&#34;}
</span></span><span class="line"><span class="cl">{&#34;m.homeserver&#34;:{&#34;base_url&#34;:&#34;https://YOUR_DOMAIN/&#34;}}</span></span></code></pre></div>
</div>
<p>If one of the three returns HTML instead of JSON, Element intercepted the request. Then the
<code>priority</code> is wrong. If the first one returns JSON instead of HTML, it is the other way round.</p>
<h3 id="step-5-the-first-login-and-device-verification">Step 5: The first login and device verification</h3>
<p>Open <code>https://YOUR_DOMAIN/</code> in the browser. Because <code>disable_custom_urls</code> is set, your server is
fixed in the mask; you only enter the username and password from part 1.</p>
<p><figure class="my-6"><img src="/en/tutorials/matrix-element-federation/el-login_hu_e23c2ccdb9633fb0.webp" srcset="/en/tutorials/matrix-element-federation/el-login_hu_e2ec2da6bfcbde02.webp 480w, /en/tutorials/matrix-element-federation/el-login_hu_e23c2ccdb9633fb0.webp 768w, /en/tutorials/matrix-element-federation/el-login_hu_2cd1641e8233c4c0.webp 1200w, /en/tutorials/matrix-element-federation/el-login_hu_3eea9ea60cd43d7a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/matrix-element-federation/el-login_hu_ea516308e3fd4b4e.webp"
    alt="The Element Web sign-in mask with a fixed homeserver and the fields for username and password" title="Element only asks for username and password – the homeserver is fixed"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Element only asks for username and password – the homeserver is fixed</figcaption></figure></p>
<p>On the very first device Element sets up encryption by itself, without asking: in the background
it creates a cross-signing key that marks your devices as yours, and a key backup on the
server. The server only stores encrypted copies of those; it cannot read them. All you see of it
is a prompt in the top left:</p>
<p><figure class="my-6"><img src="/en/tutorials/matrix-element-federation/el-backup-prompt_hu_9c86d22b48386748.webp" srcset="/en/tutorials/matrix-element-federation/el-backup-prompt_hu_a01370c5b2f88b5c.webp 480w, /en/tutorials/matrix-element-federation/el-backup-prompt_hu_9c86d22b48386748.webp 768w, /en/tutorials/matrix-element-federation/el-backup-prompt_hu_4eb6a89021b0a234.webp 1200w, /en/tutorials/matrix-element-federation/el-backup-prompt_hu_f26383e9fbb898b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/matrix-element-federation/el-backup-prompt_hu_370248c433d82c5a.webp"
    alt="Element after the first sign-in with the prompt Back up your chats and the buttons Dismiss and Continue" title="The prompt after the first login – carry on with Continue, not Dismiss"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The prompt after the first login – carry on with Continue, not 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>Do not dismiss the key backup prompt
  </p>
  <div class="prose-kitchen text-sm"><p>The prompt &ldquo;Back up your chats&rdquo; says: &ldquo;Your chats are backed up automatically. You&rsquo;ll need your
recovery key to restore them if you lose all your devices.&rdquo; So the backup is already running,
but without a recovery key you will never get at it again.</p>
<p>Take <strong>Continue</strong>. Element opens the settings under &ldquo;Encryption&rdquo;; there &ldquo;Get recovery key&rdquo; and
&ldquo;Continue&rdquo; lead to &ldquo;Save your recovery key somewhere safe&rdquo; with the key. It belongs in your
password manager. Click &ldquo;Dismiss&rdquo; instead and you have none.</p>
<p>That only surfaces when you sign in on another device. Element then asks with &ldquo;Confirm your
digital identity&rdquo;: &ldquo;Use another device&rdquo; or &ldquo;Use recovery key&rdquo;. Without a key the second option is
greyed out, and once all old devices are gone only &ldquo;Can&rsquo;t confirm?&rdquo; remains, which resets your
identity, and the history that lives only on the server can no longer be deciphered. Not even by
you as the server administrator: a <code>pg_dump</code> saves the messages, not the keys for them.</p>
</div>
</div>
<h3 id="step-6-the-first-encrypted-room">Step 6: The first encrypted room</h3>
<p>Via &ldquo;New room&rdquo; you create a room. Two settings decide things here that are fixed later on:</p>
<p><figure class="my-6"><img src="/en/tutorials/matrix-element-federation/el-new-room_hu_69a8004cecb659af.webp" srcset="/en/tutorials/matrix-element-federation/el-new-room_hu_f8cc46de402fccd4.webp 480w, /en/tutorials/matrix-element-federation/el-new-room_hu_69a8004cecb659af.webp 768w, /en/tutorials/matrix-element-federation/el-new-room_hu_43ee10a008fbc61.webp 1200w, /en/tutorials/matrix-element-federation/el-new-room_hu_e07cc5738af5906a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/matrix-element-federation/el-new-room_hu_8f156acb1aefad22.webp"
    alt="The dialog Create a private room with a filled-in name, the selection Invite only and end-to-end encryption enabled" title="Invite only can be changed later, encryption cannot"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Invite only can be changed later, encryption cannot</figcaption></figure></p>
<ul>
<li>&ldquo;Invite only&rdquo;: The visibility can be changed later.</li>
<li>&ldquo;Enable end-to-end encryption&rdquo;: That one <strong>cannot</strong> be taken back. Element says so as
well: &ldquo;You can&rsquo;t disable this later. Bridges &amp; most bots won&rsquo;t work yet.&rdquo; An encrypted room is unusable for most bridges to other
networks and for many bots. If you are planning a Telegram or IRC bridge later, create an
unencrypted room for it.</li>
</ul>
<p>After that the room is there, and the first message goes out:</p>
<p><figure class="my-6"><img src="/en/tutorials/matrix-element-federation/el-room_hu_831210f44e4c2d2b.webp" srcset="/en/tutorials/matrix-element-federation/el-room_hu_aeb7e98ec796f94a.webp 480w, /en/tutorials/matrix-element-federation/el-room_hu_831210f44e4c2d2b.webp 768w, /en/tutorials/matrix-element-federation/el-room_hu_264ef3d18833b076.webp 1200w, /en/tutorials/matrix-element-federation/el-room_hu_408f8faee3cb4875.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/matrix-element-federation/el-room_hu_d0c48947b08dd8a6.webp"
    alt="An encrypted Matrix room in Element with the notice Encryption enabled and a first message in the timeline" title="The finished room: Encryption enabled sits as a system message at the start of the timeline"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The finished room: Encryption enabled sits as a system message at the start of the timeline</figcaption></figure></p>
<p>Through the admin API you can counter-check that the server really records the room as encrypted:</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://YOUR_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 YOUR_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: Cooking night | members: 1 | encryption: m.megolm.v1.aes-sha2</span></span></code></pre></div>
</div>
<p><code>m.megolm.v1.aes-sha2</code> is Matrix&rsquo;s group encryption scheme. If it says <code>null</code> there, the room is
readable in the clear, for the server and for anyone who gets hold of the database.</p>
<h3 id="step-7-let-friends-in-without-opening-registration">Step 7: Let friends in without opening registration</h3>
<p>Part 1 sets <code>enable_registration: false</code>. That is right as long as you are alone. Only you would
have to create every further account by hand and send the password over chat. The better way are
registration tokens: registration is open, but only with an invite code that you hand out.</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>Now you create a token, here one that may be used three times:</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://YOUR_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 YOUR_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>That string is what you send your people. Element asks for it during registration. The easiest way
to check that nothing works without a token is at the start of the registration flow:</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://YOUR_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>The server now demands two stages, and the first one is the token. Without it (or with a wrong one, in
which case the server reports &ldquo;Invalid registration token&rdquo;) the flow ends without an account
being created. After a successful registration you can see the usage:</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://YOUR_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 YOUR_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> are finished registrations, <code>pending</code> are started ones. Once the token is used up it
simply stops working; you do not have to switch anything off. With <code>&quot;expiry_time&quot;</code> (milliseconds
since 1970) you can additionally give it an expiry date.</p>
<h3 id="step-8-federation-in-daily-use">Step 8: Federation in daily use</h3>
<p>The federation test from part 1 checks whether others can reach your server. The other direction,
whether your server gets out, you check by asking about someone who only exists elsewhere:</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://YOUR_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 YOUR_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>Your server cannot have made that answer up: it asked matrix.org, checked the signature and passed
the result on. The same works with room addresses; the directory entry additionally tells you which
servers sit in the room.</p>
<p>What that costs shows when you join a real public room:</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://YOUR_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 YOUR_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>Two seconds. In the two minutes after that, while Synapse loads the room state (a good 5,000
state events), its memory climbs from around 120 MB to around 280 MB, the database from 15 to
63 MB. For a single room. That is the most important sentence about Matrix federation: your server
keeps the complete state of every room you sit in, including the part that comes from foreign
servers. A homeserver with 1 GB of RAM is therefore quickly finished once somebody joins three large
rooms.</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>Room IDs have changed their format
  </p>
  <div class="prose-kitchen text-sm">The room ID above has no colon and no server name any more. From room version 12 onwards, room IDs
are pure checksums without an origin. Old IDs of the form <code>!abcdef:matrix.org</code> stay valid. Scripts
that rely on the colon do not.</div>
</div>
<h3 id="step-9-day-to-day-admin">Step 9: Day-to-day admin</h3>
<p>There are three moves you need regularly. <strong>Retiring accounts.</strong> An account is never deleted but
deactivated, because the user ID lives on in foreign rooms:</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://YOUR_DOMAIN/_synapse/admin/v1/deactivate/@NAME:YOUR_DOMAIN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer YOUR_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> additionally removes the display name and avatar. The user ID stays blocked and cannot
be handed out again. That is on purpose, otherwise someone could take over the identity.</p>
<p><strong>Getting rid of rooms</strong> you no longer need. First leave and forget, then purge from the server:</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">ROOM</span><span class="o">=</span><span class="s1">&#39;!YOUR_ROOM_ID&#39;</span>
</span></span><span class="line"><span class="cl">curl -s -X DELETE <span class="s2">&#34;https://YOUR_DOMAIN/_synapse/admin/v2/rooms/</span><span class="nv">$ROOM</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 YOUR_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>The call returns immediately, the cleanup runs in the background. You query the state at
<code>/_synapse/admin/v2/rooms/delete_status/DELETE_ID</code> until it says <code>&quot;status&quot;:&quot;complete&quot;</code>. With
<code>&quot;block&quot;: true</code> you additionally block the room so nobody can join it again.</p>
<p><strong>Cleaning up media.</strong> Every image from every federated room ends up as a copy on your disk:</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">BEFORE</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://YOUR_DOMAIN/_synapse/admin/v1/media/YOUR_DOMAIN/delete?before_ts=</span><span class="nv">$BEFORE</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 YOUR_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> is a timestamp in <strong>milliseconds</strong>; the calculation above means &ldquo;older than 30 days&rdquo;.
Forget the three zeros and the call deletes nothing, because the point in time lands in 1970.</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>The admin API does not belong on the open internet
  </p>
  <div class="prose-kitchen text-sm">Every call in this section hangs off a single token. Whoever has it can deactivate accounts and
delete rooms. Restrict <code>/_synapse/admin</code> in the reverse proxy to your VPN or your fixed address; the
pattern for that is in
<a href="/en/tutorials/internal-services-vpn-only/">Make internal services reachable only over VPN</a>.</div>
</div>
<h2 id="when-it-doesnt-work">When it doesn&rsquo;t work</h2>
<div class="troubleshoot not-prose">
<p><strong><code>/_matrix/client/versions</code> returns the Element start page instead of JSON.</strong> Both routers listen on the same host and Traefik checked the wrong one first. Set <code>traefik.http.routers.synapse.priority=100</code> and <code>…element.priority=10</code>. Without explicit numbers Traefik sorts by rule length, and that result changes as soon as another rule joins. After the change <code>docker compose up -d</code> is enough; Traefik re-reads the labels straight away.</p>
<p><strong>The Element container does not start and reports <code>cp: can't open '/app/config.json': Permission denied</code>.</strong> The <code>element-config.json</code> on the host is not readable for the container. This happens reliably on servers with <code>umask 027</code>, because new files then get <code>0640</code> and no read bit for others. <code>chmod 0644 /opt/matrix/element-config.json</code> and restart. The same applies to every configuration file you hand into a container as a bind mount.</p>
<p><strong>Element hangs at &ldquo;Confirm your digital identity&rdquo;, and &ldquo;Use recovery key&rdquo; is greyed out.</strong> You are signing in on another device and did not create a recovery key on the first one (step 5). If an old device is still signed in, take &ldquo;Use another device&rdquo; and confirm there. Otherwise only &ldquo;Can&rsquo;t confirm?&rdquo; followed by your account password remains: Element then resets the identity; account, contacts and rooms stay, but messages from before remain unreadable. Create the recovery key right afterwards.</p>
<p><strong>Signing in suddenly fails with <code>M_LIMIT_EXCEEDED</code>.</strong> The answer contains <code>retry_after_ms</code>, the waiting time in milliseconds. Synapse rate-limits login attempts per account and per IP; that hits you when you build several sessions in a row or test with scripts. For permanent operation it can be loosened through the <code>rc_login</code> block in <code>homeserver.yaml</code>, only you should not, because that exact brake is what stops guessing attacks.</p>
<p><strong>The device list and the logs show <code>172.x.x.x</code> instead of the real visitor IP.</strong> This only affects requests over <strong>IPv6</strong>, which, on a domain with an AAAA record, is most of them. The <code>proxy</code> network is created with <code>docker network create proxy</code> in the Traefik tutorial, and that is an IPv4-only network. Docker then cannot rewrite an incoming IPv6 connection onto the container directly and sends it through a helper process that replaces the source address with the bridge gateway&rsquo;s; over IPv4 the address is correct the whole time. The fix is a network with IPv6: <code>docker compose down</code> in every stack, then <code>docker network rm proxy &amp;&amp; docker network create --ipv6 --subnet fd00:cafe::/64 proxy</code> and bring everything back up; the steps are in <a href="/en/tutorials/traefik-reverse-proxy/">Reverse proxy with Traefik</a>. Afterwards the real address shows up in <code>X-Forwarded-For</code> over IPv4 <strong>and</strong> IPv6.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p>You update Element and Synapse separately. Element Web is a collection of static files with no
state of its own; an update is low-risk and takes seconds:</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:vNEW_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://YOUR_DOMAIN/version</span></span></code></pre></div>
</div>
<p>The last call prints the running version, for example <code>1.12.33</code>. Element follows Synapse loosely; a
version jump on the server does not break the interface. The reverse holds too, as long as you do
not skip several years.</p>
<p>What has to go into the backup on top of part 1:</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   # lives in no database</span></span></code></pre></div>
</div>
<p>It is a small file, but it holds every decision from step 2. If it is lost, Element starts with the
default configuration, and your people suddenly face an input field into which they can type any
homeserver at all.</p>
<p>Registration tokens need hygiene. A token with <code>uses_allowed: 3</code> and no expiry date is an invite
code that stays valid for years. Look at the list every few months and delete what you no longer
need:</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://YOUR_DOMAIN/_synapse/admin/v1/registration_tokens/THE_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer YOUR_TOKEN&#34;</span></span></span></code></pre></div>
</div>
<p>Memory use is the actual maintenance task. The jump from around 120 to 280 MB for a single
federated room scales onwards. Set yourself a reminder to look at <code>docker stats --no-stream</code> and
<code>du -sh /opt/matrix/data/media_store</code> every few months, and leave rooms where none of you writes any
more. That is more effective than any fine-tuning.</p>
<p>Realistically, Element itself creates no work: two or three updates a year. The time goes
into the homeserver: a Synapse update every two weeks with a look at the release notes, a media and
room cleanup once a quarter. Together roughly one hour a month.</p>
]]></content:encoded></item></channel></rss>