<?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>Traefik – Serverküche</title><link>https://serverkueche.de/tags/traefik/</link><description>Traefik – 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>Mon, 20 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/traefik/index.xml" rel="self" type="application/rss+xml"/><item><title>Immich selbst hosten: dein privates Foto-Backup hinter Traefik</title><link>https://serverkueche.de/tutorials/immich-fotos-selbst-hosten/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/immich-fotos-selbst-hosten/</guid><description>Immich mit Docker hinter Traefik aufsetzen: die selbst gehostete Alternative zu Google Fotos – mit automatischem Handy-Backup, Gesichtserkennung und HTTPS.</description><content:encoded><![CDATA[<p>Tausende Fotos und Videos vom Handy – und alle liegen bei Google oder Apple.
<strong>Immich</strong> holt sie zurück auf deinen Server: ein selbst gehosteter Foto-Dienst, der
Google Fotos verblüffend nahekommt, inklusive automatischem Handy-Upload, Zeitleiste,
Suche und Gesichtserkennung. In diesem Rezept setzen wir Immich sauber hinter Traefik auf.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Immich v3</strong> hinter deinem Traefik-Proxy, erreichbar unter
<code>https://photos.DEINE_DOMAIN</code> mit gültigem HTTPS. Die offizielle <strong>Immich-Handy-App</strong>
sichert deine neuen Fotos und Videos dann automatisch auf deinen Server – wie die
Cloud-Sicherung von Google Fotos, nur dass die Bilder ausschließlich bei dir liegen.
Über die Web-Oberfläche durchstöberst du deine Zeitleiste, suchst per Volltext
(„Strand&quot;, „Hund&quot;) und lässt Immich Gesichter gruppieren.</p>
<p>Immich besteht aus <strong>vier Containern</strong>: dem Server (Web + API), einem
Machine-Learning-Dienst (für Suche und Gesichtserkennung), einer PostgreSQL-Datenbank
mit Vektor-Erweiterung und einem Cache (Valkey). Klingt nach viel – die offizielle
Vorlage nimmt uns aber den Großteil ab, und wir hängen davon nur den Server an Traefik.</p>
<p>Der Reiz gegenüber Google Fotos: Die Bilder verlassen deinen Server nicht, es gibt keine
Speicherplatz-Abos und keine automatische Auswertung deiner Aufnahmen durch einen Konzern.
Dafür trägst du die Verantwortung für Betrieb und – ganz wichtig – für <strong>Backups</strong>: Wenn
der Server stirbt und du keine Sicherung hast, sind die Fotos weg. Genau deshalb steht das
Backup weiter unten nicht als Kür, sondern als Pflicht.</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>Immich ist schnelllebig – Version pinnen
  </p>
  <div class="prose-kitchen text-sm">Immich entwickelt sich rasant. Datenbank-Migrationen laufen beim Update automatisch und
sind <strong>nicht rückwärtskompatibel</strong> – ein Downgrade ist danach nicht mehr sauber möglich.
Pinne deshalb <strong>immer eine feste Version</strong> (hier <code>v3.0.3</code>) statt <code>release</code> oder <code>latest</code>,
und mache <strong>vor jedem Update ein Backup</strong>. So bestimmst du, wann aktualisiert wird.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein laufender <strong>Traefik-Reverse-Proxy</strong> mit dem <code>proxy</code>-Netzwerk und dem
Let&rsquo;s-Encrypt-Resolver <code>le</code> – siehe
<a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a>.</li>
<li>Eine Subdomain <code>photos.DEINE_DOMAIN</code> mit DNS-Record auf deine Server-IP – siehe
<a href="/tutorials/domain-mit-server-verbinden/">Domain mit Server verbinden</a>.</li>
<li>Ein <strong>Backup</strong>. Deine Fotosammlung ist unersetzlich – richte zuerst
<a href="/tutorials/backups-mit-restic/">Backups mit Restic</a> ein.</li>
</ul>
<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>Wie groß muss der Server sein?
  </p>
  <div class="prose-kitchen text-sm">Immich ist der ressourcenhungrigste Dienst der Serverküche. Offiziell werden
<strong>mindestens 6 GB RAM (empfohlen 8)</strong> verlangt – vor allem der Machine-Learning-Dienst
für Gesichtserkennung und Suche braucht Speicher. Der getestete <strong>VPS 1000 mit 8 GB RAM</strong>
erfüllt das gerade, hat aber wenig Reserve. Für eine große Bibliothek oder mehrere Nutzer
ist ein Server mit <strong>16 GB RAM</strong> (z. B. VPS 2000) entspannter. Auf einem 4-GB-Server
läuft Immich nur mit <strong>abgeschaltetem ML</strong> (siehe „Wenn es nicht funktioniert&quot;).
Und: Fotos brauchen Platz – plane genügend Festplatte ein.</div>
</div>
<p>Wie viel RAM deine Foto-Bibliothek samt KI-Suche wirklich braucht, rechnet dir der
<a href="/serverempfehlung/">Server-Rechner</a> aus.</p>
<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">
  <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</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCore · 16 GB RAM · 512 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 19,24 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Für eine große Foto-Bibliothek mit Gesichtserkennung sind 16 GB RAM angenehm.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-2000-g12-12m?ref=44083" rel="sponsored noopener" target="_blank"
   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"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 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">
  <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-dns-record-anlegen">Schritt 1: DNS-Record anlegen</h3>
<p>Lege einen Eintrag <code>photos.DEINE_DOMAIN</code> an, der auf deine Server-IP zeigt, und prüfe ihn:</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">dig +short photos.DEINE_DOMAIN</span></span></code></pre></div>
</div>
<p>Es muss deine Server-IP zurückkommen – sonst holt Traefik später kein Zertifikat.</p>
<h3 id="schritt-2-ordner-und-env-vorbereiten">Schritt 2: Ordner und .env vorbereiten</h3>
<p>Immich wird über eine <code>.env</code>-Datei konfiguriert. Leg das Projekt an:</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 ~/immich <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/immich</span></span></code></pre></div>
</div>
<p>Erstelle eine <code>.env</code> mit den Kernwerten (ein starkes DB-Passwort einsetzen – <strong>nur
Buchstaben und Ziffern</strong>, keine Sonderzeichen, das mag Immichs DB-Init nicht):</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">UPLOAD_LOCATION</span><span class="o">=</span><span class="s">./library</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_DATA_LOCATION</span><span class="o">=</span><span class="s">./postgres</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_PASSWORD</span><span class="o">=</span><span class="s">DEIN_DB_PASSWORT</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_USERNAME</span><span class="o">=</span><span class="s">postgres</span>
</span></span><span class="line"><span class="cl"><span class="na">DB_DATABASE_NAME</span><span class="o">=</span><span class="s">immich</span>
</span></span><span class="line"><span class="cl"><span class="na">IMMICH_VERSION</span><span class="o">=</span><span class="s">v3.0.3</span></span></span></code></pre></div>
</div>
<ul>
<li><strong><code>UPLOAD_LOCATION</code></strong> – hier landen deine <strong>Fotos und Videos</strong>. Das ist das
wichtigste Verzeichnis fürs Backup.</li>
<li><strong><code>DB_DATA_LOCATION</code></strong> – die PostgreSQL-Daten. Muss auf einer <strong>lokalen Platte</strong> liegen
(kein Netzlaufwerk/NFS – die DB nimmt das übel).</li>
<li><strong><code>IMMICH_VERSION</code></strong> – die gepinnte Version.</li>
</ul>
<h3 id="schritt-3-die-composeyaml-anlegen">Schritt 3: Die compose.yaml anlegen</h3>
<p>Wir übernehmen die offizielle Immich-Vorlage und ergänzen nur die Traefik-Labels am
<code>immich-server</code>. Erstelle <code>compose.yaml</code>:</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="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">immich</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">immich-server</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/immich-app/immich-server:${IMMICH_VERSION}</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">${UPLOAD_LOCATION}:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/localtime:/etc/localtime:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</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">DB_HOSTNAME</span><span class="p">:</span><span class="w"> </span><span class="l">database</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REDIS_HOSTNAME</span><span class="p">:</span><span class="w"> </span><span class="l">redis</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">redis</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_started</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><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">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">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.immich.rule=Host(`photos.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.immich.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.immich.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.immich.loadbalancer.server.port=2283&#34;</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">default, proxy]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">immich-machine-learning</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/immich-app/immich-machine-learning:${IMMICH_VERSION}</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">model-cache:/cache</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">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</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">docker.io/valkey/valkey:9</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">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><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">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0</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_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_PASSWORD}</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">${DB_USERNAME}</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">${DB_DATABASE_NAME}</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">${DB_DATA_LOCATION}:/var/lib/postgresql/data</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 ${DB_USERNAME} -d ${DB_DATABASE_NAME}&#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">5</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">default]</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></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">model-cache</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">default</span><span class="p">:</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>Wichtig zu verstehen:</p>
<ul>
<li><strong>Nur <code>immich-server</code> hängt im <code>proxy</code>-Netz</strong> und trägt Traefik-Labels. Der ML-Dienst,
Valkey und die Datenbank bleiben ausschließlich im internen <code>default</code>-Netz – von außen
nicht erreichbar.</li>
<li><strong><code>loadbalancer.server.port=2283</code></strong> – Immich lauscht im Container auf Port 2283.</li>
<li>Die <strong>Datenbank</strong> ist bewusst das vorgebaute Immich-Postgres-Image mit der
Vektor-Erweiterung <em>VectorChord</em> (für die Bildsuche). Nimm nicht einfach ein
Standard-<code>postgres</code> – die Erweiterung fehlt dann.</li>
<li><strong>Valkey</strong> ist der Redis-Nachfolger; der Service heißt in der Immich-Vorlage aus
Kompatibilitätsgründen weiterhin <code>redis</code>.</li>
<li>Der <strong>Healthcheck</strong> an der Datenbank meldet, wann Postgres wirklich bereit ist. Über
<code>depends_on: … condition: service_healthy</code> startet <code>immich-server</code> erst dann – das
verhindert Migrationsfehler und den „Datenbank nicht erreichbar&quot;-Start beim ersten Hochfahren.</li>
</ul>
<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>Änderungen an der .env übernehmen
  </p>
  <div class="prose-kitchen text-sm">Wenn du später etwas in der <code>.env</code> änderst, reicht ein <code>docker compose restart</code> <strong>nicht</strong> –
Docker liest die Umgebungsvariablen nur beim <strong>Neuerstellen</strong> der Container. Nutze dann
<code>docker compose up -d</code>; Compose erkennt die Änderung und baut die betroffenen Container neu.</div>
</div>
<h3 id="schritt-4-große-uploads-erlauben-traefik-timeout">Schritt 4: Große Uploads erlauben (Traefik-Timeout)</h3>
<p>Das ist <strong>der</strong> Stolperstein bei Immich hinter Traefik: Standardmäßig bricht Traefik
Verbindungen nach <strong>60 Sekunden</strong> ab (<code>readTimeout</code>). Beim Hochladen großer Videos vom
Handy führt das zu abgebrochenen Uploads (Fehler 502/499). Erhöhe den Timeout am
<code>websecure</code>-Entrypoint in deiner <strong>Traefik-Konfiguration</strong> (der <code>traefik</code>-Service aus dem
<a href="/tutorials/reverse-proxy-traefik/">Traefik-Tutorial</a>):</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">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># ... deine bestehenden Zeilen ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--entrypoints.websecure.transport.respondingTimeouts.readTimeout=600s&#34;</span></span></span></code></pre></div>
</div>
<p>Starte Traefik danach neu (<code>docker compose up -d</code> im Traefik-Ordner). Anders als nginx hat
Traefik kein festes Größenlimit für Uploads – nur dieser Timeout muss hoch.</p>
<h3 id="schritt-5-starten-und-den-erst-setup-durchlaufen">Schritt 5: Starten und den Erst-Setup durchlaufen</h3>
<p>Zieh die Images (mehrere GB – das dauert) und starte:</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 logs -f immich-server</span></span></code></pre></div>
</div>
<p>Die Images sind mehrere Gigabyte groß – der erste <code>pull</code> braucht je nach Anbindung ein
paar Minuten. Prüfe danach, dass alle vier Container laufen:</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 ps</span></span></code></pre></div>
</div>
<p>Du solltest <code>immich-server</code>, <code>immich-machine-learning</code>, <code>redis</code> und <code>database</code> mit Status
<code>running</code> (die Datenbank <code>healthy</code>) sehen. Warte, bis im Log
<code>Immich Server is listening on http://[::1]:2283 [v3.0.3]</code> erscheint.
Ruf dann <code>https://photos.DEINE_DOMAIN</code> auf. Traefik holt beim ersten Zugriff das
Let&rsquo;s-Encrypt-Zertifikat (einen Moment Geduld). Beim allerersten Start begrüßt dich Immich mit
<strong>„Willkommen bei Immich&quot;</strong> und der Wahl, ob du <strong>neu startest</strong> oder eine Sicherung
wiederherstellst:</p>
<p>![Der Willkommensbildschirm von Immich mit den Optionen „Erste Schritte&quot; und „Von Datenbank wiederherstellen&quot;](immich-willkommen.png &ldquo;Erster Aufruf: „Erste Schritte&rdquo; wählen für eine frische Installation&quot;)</p>
<p>Klick auf <strong>Erste Schritte</strong> (Wiederherstellen brauchst du nur, wenn du ein Backup
einspielst). Danach legst du das <strong>Administrator-Konto</strong> an – als erster Nutzer wirst du
automatisch Admin:</p>
<p><figure class="my-6"><img src="/tutorials/immich-fotos-selbst-hosten/immich-registrierung_hu_9eb529b3092ecb0f.webp" srcset="/tutorials/immich-fotos-selbst-hosten/immich-registrierung_hu_b01ab1b78a362cf8.webp 480w, /tutorials/immich-fotos-selbst-hosten/immich-registrierung_hu_9eb529b3092ecb0f.webp 768w, /tutorials/immich-fotos-selbst-hosten/immich-registrierung_hu_e1ac72235e36411a.webp 1200w, /tutorials/immich-fotos-selbst-hosten/immich-registrierung_hu_6c10d20e7518a92e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/immich-fotos-selbst-hosten/immich-registrierung_hu_21707351f7f5a98d.webp"
    alt="Das Immich-Formular „Admin-Registrierung&quot; mit Feldern für E-Mail, Passwort und Name" title="Admin-Registrierung – der erste Nutzer wird automatisch Administrator"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Admin-Registrierung – der erste Nutzer wird automatisch Administrator</figcaption></figure></p>
<p>Nach dem ersten Login führt dich ein kurzer <strong>Einrichtungs-Assistent</strong> durch Theme,
Sprache und Datenschutz-Grundeinstellungen:</p>
<p><figure class="my-6"><img src="/tutorials/immich-fotos-selbst-hosten/immich-onboarding_hu_f414a63437dc818.webp" srcset="/tutorials/immich-fotos-selbst-hosten/immich-onboarding_hu_fa155ce76ab7d4ae.webp 480w, /tutorials/immich-fotos-selbst-hosten/immich-onboarding_hu_f414a63437dc818.webp 768w, /tutorials/immich-fotos-selbst-hosten/immich-onboarding_hu_e1c128859e8abc5.webp 1200w, /tutorials/immich-fotos-selbst-hosten/immich-onboarding_hu_b32c5bf3ebed1c7e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/immich-fotos-selbst-hosten/immich-onboarding_hu_93e44cae02bd6aa4.webp"
    alt="Der Immich-Einrichtungsassistent begrüßt den neuen Administrator" title="Der Onboarding-Assistent nach dem ersten Login"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der Onboarding-Assistent nach dem ersten Login</figcaption></figure></p>
<p>Danach landest du auf deiner (noch leeren) <strong>Zeitleiste</strong> – das Herzstück von Immich:</p>
<p><figure class="my-6"><img src="/tutorials/immich-fotos-selbst-hosten/immich-timeline_hu_22e64ce51c6c0a00.webp" srcset="/tutorials/immich-fotos-selbst-hosten/immich-timeline_hu_f6682d8a8fbc043f.webp 480w, /tutorials/immich-fotos-selbst-hosten/immich-timeline_hu_22e64ce51c6c0a00.webp 768w, /tutorials/immich-fotos-selbst-hosten/immich-timeline_hu_dd26bf995c060373.webp 1200w, /tutorials/immich-fotos-selbst-hosten/immich-timeline_hu_6b803b47ac9db0b8.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/immich-fotos-selbst-hosten/immich-timeline_hu_bdb998e8763cf029.webp"
    alt="Die leere Foto-Zeitleiste von Immich mit der Aufforderung, das erste Foto hochzuladen" title="Die Foto-Zeitleiste – hier laufen später alle Bilder ein (Server online, v3.0.3)"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Foto-Zeitleiste – hier laufen später alle Bilder ein (Server online, v3.0.3)</figcaption></figure></p>
<h3 id="schritt-6-die-handy-app-verbinden">Schritt 6: Die Handy-App verbinden</h3>
<p>Der eigentliche Nutzen kommt von der App. Installiere <strong>Immich</strong> aus dem App Store bzw.
Play Store (oder F-Droid). Beim Start fragt sie nach der <strong>Server-Adresse</strong> – trag
<code>https://photos.DEINE_DOMAIN</code> ein und melde dich mit deinem Konto an. Aktiviere dann in den
App-Einstellungen die <strong>Sicherung</strong> und wähle die Alben, die hochgeladen werden sollen.</p>
<p>Ab jetzt sichert dein Handy neue Fotos automatisch auf deinen Server – im Hintergrund und
über WLAN. Der erste Durchlauf einer großen Mediathek dauert; danach kommen nur noch neue
Aufnahmen dazu.</p>
<p>Nach dem Upload arbeitet Immich im Hintergrund weiter: Es erzeugt Vorschaubilder, liest
die Aufnahme-Metadaten (Datum, Ort) aus und lässt den ML-Dienst <strong>Gesichter erkennen</strong> und
die Bilder für die <strong>intelligente Suche</strong> indexieren. Diese Jobs laufen einige Zeit nach
dem ersten großen Import – Gesichter und Suchtreffer tauchen also erst nach und nach auf.
Den Fortschritt siehst du unter <strong>Administration → Auftragswarteschlangen</strong>. In der
Seitenleiste findest du dann die typischen Foto-Funktionen: <strong>Erkunden</strong> (nach Personen und
Orten), die <strong>Karte</strong> mit Geo-Verortung, <strong>Alben</strong> zum Teilen und die <strong>Rückblicke</strong>
(„Vor einem Jahr&quot;).</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>Videos &amp; Transcoding auf dem VPS
  </p>
  <div class="prose-kitchen text-sm">Immich wandelt Videos für die Wiedergabe im Browser um (Transcoding). Auf einem normalen
VPS <strong>ohne Grafikkarte</strong> passiert das über die CPU – das ist bei vielen oder langen Videos
spürbar langsamer und lastet Kerne aus. Für ein privates Handy-Backup ist das in der Regel
kein Problem; wer sehr viel Video hat, sollte den Server entsprechend größer wählen.</div>
</div>
<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>Weitere Nutzer
  </p>
  <div class="prose-kitchen text-sm">Für die Familie legst du unter <strong>Administration → Benutzer</strong> weitere Konten an – jedes
bekommt seine eigene, getrennte Bibliothek. Registrierung von außen ist standardmäßig aus;
neue Nutzer entstehen nur über das Admin-Konto.</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>Deine Fotos hängen im Internet
  </p>
  <div class="prose-kitchen text-sm">Sobald Immich über <code>photos.DEINE_DOMAIN</code> erreichbar ist, steht die Anmeldeseite offen im
Netz. Vergib deshalb ein <strong>langes, einmaliges Passwort</strong> für das Admin- und alle
Nutzerkonten und aktiviere die <strong>Zwei-Faktor-Authentifizierung</strong> in den Kontoeinstellungen.
Wer ganz sichergehen will, macht Immich nur über ein VPN erreichbar – für den automatischen
Handy-Upload ist die direkte HTTPS-Erreichbarkeit aber meist der praktischere Weg.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> Uploads großer Videos brechen nach etwa einer Minute ab (Fehler 502 oder 499).</p>
<p><strong>Ursache &amp; Lösung:</strong> Traefiks <code>readTimeout</code> (Standard 60 s) greift. Setz ihn wie in
Schritt 4 auf <code>600s</code> (oder höher) und starte Traefik neu. Das ist mit Abstand der
häufigste Immich-hinter-Proxy-Fehler.</p>
<p><strong>Symptom:</strong> Der <code>immich-machine-learning</code>-Container stürzt ab bzw. beendet sich mit
„exit 137&quot;, Suche und Gesichtserkennung funktionieren nicht.</p>
<p><strong>Ursache &amp; Lösung:</strong> Zu wenig RAM – der Container wurde vom System beendet (Out of
Memory). Gib dem Server mehr Speicher, oder <strong>deaktiviere ML</strong>, indem du den Service
<code>immich-machine-learning</code> aus der <code>compose.yaml</code> entfernst. Immich läuft dann ohne
Gesichtserkennung und intelligente Suche, aber Upload und Zeitleiste funktionieren normal.</p>
<p><strong>Symptom:</strong> Nach einem Update starten die Container nicht mehr oder melden
Migrationsfehler.</p>
<p><strong>Ursache &amp; Lösung:</strong> Server, Machine-Learning und Datenbank müssen auf <strong>derselben</strong>
Version laufen. Setz in der <code>.env</code> die neue <code>IMMICH_VERSION</code> und aktualisiere <strong>alle</strong>
Container gemeinsam (<code>docker compose pull &amp;&amp; docker compose up -d</code>). Ein Downgrade nach
einer Migration ist nicht möglich – nur ein Rücksetzen aus dem Backup.</p>
<p><strong>Symptom:</strong> Immich zeigt „Wartungsmodus&quot; / „Vorübergehend nicht verfügbar&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> Immich v3 startet bei bestimmten Datenbank-Zuständen in einen
Wartungsmodus. Im Log (<code>docker compose logs immich-server</code>) steht dann eine URL mit einem
<strong>einmaligen Token</strong> (<code>…/maintenance?token=…</code>) – darüber meldest du dich am Wartungsmodus
an und wählst „neu starten&quot; bzw. beendest ihn. Danach ist die normale Oberfläche wieder da.</p>
<p><strong>Symptom:</strong> Container startet nicht, „permission denied&quot; beim Datenbank- oder
Upload-Verzeichnis.</p>
<p><strong>Ursache &amp; Lösung:</strong> <code>UPLOAD_LOCATION</code> oder <code>DB_DATA_LOCATION</code> haben die falschen
Zugriffsrechte oder liegen auf einem Netzlaufwerk. Lege beide auf einer lokalen Platte an
und stelle sicher, dass Docker hineinschreiben darf.</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li>
<p><strong>Zwei Dinge gehören ins Backup – konsistent zusammen.</strong> Deine <strong>Fotos</strong>
(<code>UPLOAD_LOCATION</code>, also <code>~/immich/library</code>) und die <strong>Datenbank</strong>. Der DB-Dump enthält
nur die Metadaten; ohne die passenden Dateien ist er wertlos, und umgekehrt. Am
saubersten stoppst du Immich kurz und sicherst beides zusammen:</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 stop immich-server
</span></span><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -T database pg_dump --clean --if-exists <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dbname<span class="o">=</span>immich --username<span class="o">=</span>postgres <span class="p">|</span> gzip &gt; immich-db.sql.gz
</span></span><span class="line"><span class="cl">docker compose start immich-server</span></span></code></pre></div>
</div>
<p>Sichere den Dump zusammen mit dem <code>library</code>-Ordner <strong>verschlüsselt und off-site</strong> mit
<a href="/tutorials/backups-mit-restic/">Restic</a>. Alternativ kann Immich unter
<strong>Administration → Auftragswarteschlangen</strong> automatische DB-Dumps erzeugen – die Fotos
musst du trotzdem separat sichern.</p>
</li>
<li>
<p><strong>Den Ernstfall einmal proben.</strong> Ein Backup, das du nie zurückgespielt hast, ist nur eine
Hoffnung. Zum Wiederherstellen spielst du den DB-Dump in eine frische Immich-Instanz
gleicher Version ein und legst den <code>library</code>-Ordner an dieselbe Stelle – Immich bietet
dafür beim ersten Start den Punkt <strong>„Von Datenbank wiederherstellen&quot;</strong> an (der zweite
Knopf aus Schritt 5). Wichtig ist die <strong>gleiche Version</strong>: Datenbank-Stand und Programm
müssen zusammenpassen.</p>
</li>
<li>
<p><strong>Backup vor jedem Update.</strong> Weil Migrationen nicht umkehrbar sind, ist das Backup dein
einziger Rückweg. Erst sichern, dann <code>IMMICH_VERSION</code> erhöhen, dann
<code>docker compose pull &amp;&amp; docker compose up -d</code>.</p>
</li>
<li>
<p><strong>Ehrlich zum Aufwand:</strong> Immich bringt oft mehrere Releases pro Monat. Du musst nicht
jedem folgen – aber lies vor einem Sprung über mehrere Versionen die Release-Notes, und
halte dich an die gepinnte Version, bis du bewusst aktualisierst.</p>
</li>
<li>
<p><strong>Speicherplatz im Blick behalten.</strong> Fotos und Videos wachsen stetig; Immich legt
zusätzlich Vorschaubilder und umgewandelte Videos an. Überwache die Plattenbelegung
(z. B. mit <a href="/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a>), damit dir der Server nicht
volläuft.</p>
</li>
</ul>
]]></content:encoded></item><item><title>Paperless-ngx selbst hosten: papierloses Büro mit OCR</title><link>https://serverkueche.de/tutorials/paperless-dokumentenverwaltung/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/paperless-dokumentenverwaltung/</guid><description>Paperless-ngx mit Docker hinter Traefik aufsetzen: Dokumente per OCR durchsuchbar archivieren – mit Volltextsuche, Tags und automatischem Import.</description><content:encoded><![CDATA[<p>Rechnungen, Verträge, Behördenpost – der Papierstapel wächst und wächst, und finden tust du
am Ende doch nie etwas. <strong>Paperless-ngx</strong> macht daraus ein durchsuchbares digitales Archiv: Du wirfst ein
Dokument hinein, es wird per <strong>OCR</strong> erkannt, verschlagwortet und ist über die
Volltextsuche in Sekunden wieder da. In diesem Rezept setzen wir es hinter Traefik auf.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Paperless-ngx 2.20</strong> hinter deinem Traefik-Proxy, erreichbar unter
<code>https://paperless.DEINE_DOMAIN</code> mit HTTPS. Paperless ist ein Dokumenten-Management-System:
Du fütterst es mit Scans oder PDFs, es liest den Text per <strong>OCR</strong> aus (auch aus reinen
Bild-Scans), erkennt Datum und Inhalt und legt alles durchsuchbar ab. Über Tags,
Korrespondenten und Dokumenttypen bringst du Ordnung hinein; die Volltextsuche findet
später jedes Dokument.</p>
<p>Der Clou ist der <strong>Consume-Ordner</strong>: Alles, was du dort ablegst (z. B. vom Netzwerk-Scanner),
wird automatisch importiert und verarbeitet. Paperless besteht aus <strong>fünf Containern</strong> –
dem Webserver, einer PostgreSQL-Datenbank, einem Redis-Broker für die
Hintergrundverarbeitung sowie <strong>Gotenberg</strong> und <strong>Tika</strong>, die Office-Dokumente in PDF
umwandeln. Die offizielle Vorlage liefert das komplette Gespann.</p>
<p>Der Gewinn gegenüber einem Ordner voller PDFs auf der Festplatte: Paperless macht jedes
Dokument <strong>durchsuchbar</strong> (auch eingescanntes Papier), hält Original und Archivversion
sauber getrennt und lässt sich per Regeln automatisieren. Deine Unterlagen bleiben dabei
auf deinem Server – kein Cloud-Dienst liest mit, und du bist nicht an ein proprietäres
Format gebunden.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein laufender <strong>Traefik-Reverse-Proxy</strong> mit dem <code>proxy</code>-Netzwerk und dem Resolver <code>le</code>
– siehe <a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a>.</li>
<li>Eine Subdomain <code>paperless.DEINE_DOMAIN</code> mit DNS-Record auf deine Server-IP – siehe
<a href="/tutorials/domain-mit-server-verbinden/">Domain mit Server verbinden</a>.</li>
<li>Ein <strong>Backup</strong>. In Paperless landen deine wichtigsten Unterlagen – richte zuerst
<a href="/tutorials/backups-mit-restic/">Backups mit Restic</a> ein.</li>
</ul>
<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>Wie groß muss der Server sein?
  </p>
  <div class="prose-kitchen text-sm">Paperless selbst ist genügsam, aber es sind fünf Container, und die <strong>OCR-Verarbeitung ist
CPU-lastig</strong>: Beim Erkennen eines Scans läuft ein Kern für einige Sekunden bis Minuten auf
Anschlag. Für den privaten Gebrauch reichen <strong>2 vCPU und 4 GB RAM</strong> gut; der getestete VPS
1000 (4 vCore / 8 GB) hat reichlich Luft. Beim Massenimport vieler Dokumente merkst du die
CPU-Last – dann verarbeitet Paperless die Warteschlange eben nach und nach ab.</div>
</div>
<p>Unsicher, welche Server-Größe reicht? Der <a href="/serverempfehlung/">Server-Rechner</a> rechnet dir
RAM- und CPU-Bedarf für deine Dienste aus.</p>
<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">
  <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 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 10,36 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Der VPS 1000 stemmt Paperless samt OCR locker.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12-12m?ref=44083" rel="sponsored noopener" target="_blank"
   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"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 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">
  <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-dns-record-anlegen">Schritt 1: DNS-Record anlegen</h3>
<p>Lege <code>paperless.DEINE_DOMAIN</code> an (A/AAAA auf deine Server-IP) und prüfe:</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">dig +short paperless.DEINE_DOMAIN</span></span></code></pre></div>
</div>
<p>Es muss deine Server-IP zurückkommen – sonst holt Traefik später kein Zertifikat.</p>
<h3 id="schritt-2-einen-secret-key-erzeugen">Schritt 2: Einen Secret Key erzeugen</h3>
<p>Paperless verschlüsselt Sitzungen mit einem geheimen Schlüssel. Der Default ist <strong>öffentlich
bekannt</strong> – bei einer Instanz im Internet ein echtes Risiko. Erzeuge einen eigenen:</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">head -c <span class="m">50</span> /dev/urandom <span class="p">|</span> base64</span></span></code></pre></div>
</div>
<p>Kopiere die Ausgabe – sie kommt gleich als <code>PAPERLESS_SECRET_KEY</code> in die Konfiguration.</p>
<h3 id="schritt-3-die-composeyaml-anlegen">Schritt 3: Die compose.yaml anlegen</h3>
<p>Leg das Projekt an – <strong>inklusive</strong> der beiden Bind-Mount-Ordner:</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 ~/paperless/<span class="o">{</span>consume,export<span class="o">}</span> <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/paperless</span></span></code></pre></div>
</div>
<p>Die Unterordner <code>consume</code> und <code>export</code> legst du bewusst <strong>jetzt, als normaler
Benutzer</strong> an: Würde erst der Docker-Daemon sie beim Start erzeugen, gehörten sie
<code>root</code> – dann könnten weder du (<code>cp</code> in den Consume-Ordner) noch Paperless selbst
(läuft via <code>USERMAP_UID</code> als UID 1000) hineinschreiben.</p>
<p>Erstelle <code>compose.yaml</code>. Ersetze <code>paperless.DEINE_DOMAIN</code>, die Passwörter und den
<code>PAPERLESS_SECRET_KEY</code>:</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="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">paperless</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">broker</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">docker.io/library/redis:8</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">redisdata:/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">default]</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></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">docker.io/library/postgres:18</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_DB</span><span class="p">:</span><span class="w"> </span><span class="l">paperless</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">paperless</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">EIN_STARKES_DB_PASSWORT</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">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">gotenberg</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">docker.io/gotenberg/gotenberg:8.34</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;gotenberg&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--chromium-disable-javascript=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--chromium-allow-list=file:///tmp/.*&#34;</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">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tika</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">docker.io/apache/tika:3.3.1.0</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">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">webserver</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/paperless-ngx/paperless-ngx:2.20.15</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 class="p">[</span><span class="l">db, broker, gotenberg, tika]</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">PAPERLESS_REDIS</span><span class="p">:</span><span class="w"> </span><span class="l">redis://broker:6379</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_DBHOST</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">PAPERLESS_DBUSER</span><span class="p">:</span><span class="w"> </span><span class="l">paperless</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_DBPASS</span><span class="p">:</span><span class="w"> </span><span class="l">EIN_STARKES_DB_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_TIKA_ENABLED</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_TIKA_GOTENBERG_ENDPOINT</span><span class="p">:</span><span class="w"> </span><span class="l">http://gotenberg:3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_TIKA_ENDPOINT</span><span class="p">:</span><span class="w"> </span><span class="l">http://tika:9998</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_URL</span><span class="p">:</span><span class="w"> </span><span class="l">https://paperless.DEINE_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_SECRET_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">DEIN_LANGER_SECRET_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_OCR_LANGUAGE</span><span class="p">:</span><span class="w"> </span><span class="l">deu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_TIME_ZONE</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">PAPERLESS_ADMIN_USER</span><span class="p">:</span><span class="w"> </span><span class="l">admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PAPERLESS_ADMIN_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">EIN_STARKES_ADMIN_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">USERMAP_UID</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">USERMAP_GID</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1000&#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">data:/usr/src/paperless/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">media:/usr/src/paperless/media</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./export:/usr/src/paperless/export</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./consume:/usr/src/paperless/consume</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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.ppl.rule=Host(`paperless.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.ppl.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.ppl.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.ppl.loadbalancer.server.port=8000&#34;</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">default, proxy]</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></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">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">media</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 class="nt">redisdata</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">default</span><span class="p">:</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 wichtigsten Punkte:</p>
<ul>
<li><strong>Nur <code>webserver</code> hängt im <code>proxy</code>-Netz</strong> und trägt Traefik-Labels (Port <strong>8000</strong>). Die
vier Hilfsdienste (db, broker, gotenberg, tika) bleiben intern.</li>
<li><strong><code>PAPERLESS_URL</code></strong> ist Pflicht hinter einem Proxy. Fehlt sie, weist Paperless den Login
mit einem CSRF-/„403 Forbidden&quot;-Fehler ab. Sie setzt zugleich die erlaubten Hosts und die
vertrauenswürdigen Ursprünge.</li>
<li><strong><code>PAPERLESS_OCR_LANGUAGE: deu</code></strong> stellt die Texterkennung auf Deutsch. Deutsch, Englisch
und ein paar weitere Sprachen sind im Image bereits enthalten – kein Zusatzpaket nötig.</li>
<li><strong><code>PAPERLESS_ADMIN_USER</code> / <code>_PASSWORD</code></strong> legen beim ersten Start automatisch den
Superuser an, sodass du dich direkt anmelden kannst.</li>
<li><strong><code>USERMAP_UID</code>/<code>GID</code></strong> sollten zur Kennung deines Server-Benutzers passen (per <code>id -u</code>
bzw. <code>id -g</code> ermitteln, meist <code>1000</code>). Sonst gibt es „Permission denied&quot; im Consume-Ordner.</li>
<li><strong><code>export</code> und <code>consume</code></strong> sind bewusst Ordner im Projektverzeichnis (Bind-Mounts):
<code>consume</code> ist der Eingangskorb, <code>export</code> das Ziel für Backups.</li>
</ul>
<p>Paperless nutzt insgesamt <strong>vier Speicherbereiche</strong>, die du auseinanderhalten solltest:
<code>media</code> enthält deine <strong>verarbeiteten Dokumente</strong> (das Herzstück!), <code>data</code> den Suchindex und
Hilfsdaten, <code>consume</code> ist der Eingangskorb und <code>export</code> das Backup-Ziel. Fürs Backup zählen
<code>media</code> und <code>data</code> sowie die Datenbank – genau das nimmt dir der <code>document_exporter</code> weiter
unten ab.</p>
<h3 id="schritt-4-starten-und-anmelden">Schritt 4: Starten und anmelden</h3>
<p>Zieh die Images (mehrere GB) und starte:</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 logs -f webserver</span></span></code></pre></div>
</div>
<p>Beim ersten Start richtet Paperless die Datenbank ein (Migrationen) – das dauert einen
Moment. Prüfe, dass alle fünf Container laufen:</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 ps</span></span></code></pre></div>
</div>
<p>Du solltest <code>webserver</code>, <code>db</code>, <code>broker</code>, <code>gotenberg</code> und <code>tika</code> mit Status <code>running</code> sehen
(der <code>webserver</code> wird nach kurzer Zeit <code>healthy</code>). Ist der Worker bereit (<code>celery@… ready</code>
im Log), ruf <code>https://paperless.DEINE_DOMAIN</code> auf. Es erscheint die Anmeldeseite:</p>
<p><figure class="my-6"><img src="/tutorials/paperless-dokumentenverwaltung/paperless-login_hu_13e9e045d044f4df.webp" srcset="/tutorials/paperless-dokumentenverwaltung/paperless-login_hu_250396419bfda38b.webp 480w, /tutorials/paperless-dokumentenverwaltung/paperless-login_hu_13e9e045d044f4df.webp 768w, /tutorials/paperless-dokumentenverwaltung/paperless-login_hu_1d250bfe93a42aaf.webp 1200w, /tutorials/paperless-dokumentenverwaltung/paperless-login_hu_2f11ff58ed680cd4.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/paperless-dokumentenverwaltung/paperless-login_hu_14bc256f3e25ec2b.webp"
    alt="Die Anmeldeseite von Paperless-ngx unter der eigenen Domain" title="Die Anmeldung – Superuser wurde per Umgebungsvariable angelegt"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Anmeldung – Superuser wurde per Umgebungsvariable angelegt</figcaption></figure></p>
<p>Melde dich mit <code>admin</code> und deinem Passwort an. Du landest auf der <strong>Startseite</strong> mit einer
kurzen Willkommensmeldung und ersten Statistiken. Ganz unten links siehst du die laufende
<strong>Version</strong> – praktisch, um vor einem Update den Ausgangsstand zu kennen. Die Sprache
kannst du bei Bedarf unter <strong>Einstellungen</strong> umstellen; standardmäßig folgt Paperless der
Sprache deines Browsers:</p>
<p><figure class="my-6"><img src="/tutorials/paperless-dokumentenverwaltung/paperless-dashboard_hu_6220515987835f18.webp" srcset="/tutorials/paperless-dokumentenverwaltung/paperless-dashboard_hu_a2ec722d2c48e2cc.webp 480w, /tutorials/paperless-dokumentenverwaltung/paperless-dashboard_hu_6220515987835f18.webp 768w, /tutorials/paperless-dokumentenverwaltung/paperless-dashboard_hu_4fa62c1cd86dc4cf.webp 1200w, /tutorials/paperless-dokumentenverwaltung/paperless-dashboard_hu_dbff5143da947ab3.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/paperless-dokumentenverwaltung/paperless-dashboard_hu_c1999cd8336d31ca.webp"
    alt="Die Startseite von Paperless-ngx mit Willkommensmeldung und Statistik-Widget" title="Die Startseite nach dem ersten Login"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Startseite nach dem ersten Login</figcaption></figure></p>
<h3 id="schritt-5-das-erste-dokument-einlesen">Schritt 5: Das erste Dokument einlesen</h3>
<p>Jetzt der Kern. Es gibt drei Wege, ein Dokument hineinzubekommen:</p>
<ol>
<li><strong>Über die Weboberfläche:</strong> oben rechts auf <strong>Dokumente hochladen</strong> und eine PDF- oder
Bilddatei auswählen.</li>
<li><strong>Über den Consume-Ordner:</strong> Leg eine Datei in <code>~/paperless/consume</code> – Paperless erkennt
sie automatisch, verarbeitet sie und löscht sie danach aus dem Ordner. Ideal für einen
Netzwerk-Scanner, der direkt dorthin scannt.</li>
<li><strong>Aus einem Postfach:</strong> Paperless kann unter <strong>E-Mail</strong> ein IMAP-Konto abrufen und
Anhänge automatisch einlesen – praktisch für Rechnungen, die dir ohnehin per Mail kommen.</li>
</ol>
<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">cp ~/eine-rechnung.pdf ~/paperless/consume/</span></span></code></pre></div>
</div>
<p>Paperless nimmt PDFs, Bilder (JPG/PNG/TIFF) und – dank Gotenberg und Tika – auch
Office-Dateien wie Word oder Excel an. Bei einem reinen Bild-Scan liest die <strong>OCR</strong> den Text
aus und legt ihn als durchsuchbare Ebene über das Dokument; bei einem PDF mit vorhandenem
Textlayer überspringt Paperless die Erkennung und ist entsprechend schneller. Das Original
bleibt dabei unangetastet erhalten – Paperless erzeugt zusätzlich eine durchsuchbare
Archiv-Version.</p>
<p>Nach ein paar Sekunden (OCR braucht etwas Zeit) taucht das Dokument unter <strong>Dokumente</strong> auf –
mit einer Vorschau und dem erkannten Titel:</p>
<p><figure class="my-6"><img src="/tutorials/paperless-dokumentenverwaltung/paperless-dokumente_hu_aa3ea386e65762ca.webp" srcset="/tutorials/paperless-dokumentenverwaltung/paperless-dokumente_hu_6d8fc0b01dfc2042.webp 480w, /tutorials/paperless-dokumentenverwaltung/paperless-dokumente_hu_aa3ea386e65762ca.webp 768w, /tutorials/paperless-dokumentenverwaltung/paperless-dokumente_hu_5a95a611ea70adf4.webp 1200w, /tutorials/paperless-dokumentenverwaltung/paperless-dokumente_hu_ae6c8e64a39d2558.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/paperless-dokumentenverwaltung/paperless-dokumente_hu_fabb843514ed759b.webp"
    alt="Die Dokumentenliste von Paperless-ngx mit einem verarbeiteten Dokument als Kachel" title="Die Dokumentenliste – das eingelesene Dokument ist sofort durchsuchbar"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Dokumentenliste – das eingelesene Dokument ist sofort durchsuchbar</figcaption></figure></p>
<p>Ein Klick öffnet die <strong>Detailansicht</strong>: links die Metadaten (Titel, Datum, Korrespondent,
Tags) und die Reiter für Inhalt, Metadaten und Verlauf, rechts das Dokument mit einer
zoombaren Vorschau. Paperless hat aus dem Scan bereits das <strong>Datum erkannt</strong> und
schlägt es zur Bestätigung vor – genau das leistet die OCR im Hintergrund für dich:</p>
<p><figure class="my-6"><img src="/tutorials/paperless-dokumentenverwaltung/paperless-dokument-detail_hu_a62a5197dcfa1428.webp" srcset="/tutorials/paperless-dokumentenverwaltung/paperless-dokument-detail_hu_853b7adaaa974b3d.webp 480w, /tutorials/paperless-dokumentenverwaltung/paperless-dokument-detail_hu_a62a5197dcfa1428.webp 768w, /tutorials/paperless-dokumentenverwaltung/paperless-dokument-detail_hu_f2c15bca02914d94.webp 1200w, /tutorials/paperless-dokumentenverwaltung/paperless-dokument-detail_hu_4898762c6bc95430.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/paperless-dokumentenverwaltung/paperless-dokument-detail_hu_5304402e81a3d611.webp"
    alt="Die Detailansicht eines Dokuments in Paperless-ngx mit Metadaten links und dem erkannten Dokument rechts" title="Detailansicht: OCR hat Text und Datum erkannt, Metadaten lassen sich ergänzen"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Detailansicht: OCR hat Text und Datum erkannt, Metadaten lassen sich ergänzen</figcaption></figure></p>
<h3 id="schritt-6-ordnung-mit-tags-korrespondenten--dokumenttypen">Schritt 6: Ordnung mit Tags, Korrespondenten &amp; Dokumenttypen</h3>
<p>Damit die Suche später greift, vergibst du <strong>Tags</strong> (z. B. <code>Steuer</code>, <code>Versicherung</code>),
ordnest einen <strong>Korrespondenten</strong> (den Absender) und einen <strong>Dokumenttyp</strong> (Rechnung,
Vertrag …) zu. Das kannst du von Hand machen – oder Paperless über <strong>Arbeitsabläufe</strong>
automatisieren: Regeln, die eingehende Dokumente anhand ihres Inhalts automatisch
verschlagworten. So sortiert sich dein Archiv mit der Zeit von selbst.</p>
<p>Die <strong>Volltextsuche</strong> oben durchsucht danach nicht nur Titel und Tags, sondern den
kompletten erkannten Text – eine Suche nach <code>Rechnungsbetrag</code> oder einem Kundennamen findet
das passende Dokument in Sekunden. Kombiniert mit den Filtern (Korrespondent, Zeitraum,
Dokumenttyp) wird der Papierstapel endgültig zum durchsuchbaren Archiv.</p>
<p>Ein einfaches Beispiel für einen <strong>Arbeitsablauf</strong>: Enthält ein neues Dokument das Wort
„Stromabrechnung&quot;, vergib automatisch den Tag <code>Energie</code>, setz den Korrespondenten auf deinen
Stromanbieter und den Dokumenttyp auf <code>Rechnung</code>. Solche Regeln legst du unter
<strong>Verwaltung → Arbeitsabläufe</strong> an; sie greifen bei jedem eingehenden Dokument. Anfangs
lohnt es sich, ein paar Dokumente von Hand zu sortieren – daraus siehst du schnell, welche
Regeln sich wiederholen und automatisieren lassen. Nach ein paar Wochen landet der Großteil
deiner Post ohne dein Zutun am richtigen Platz.</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>Scanner direkt in den Consume-Ordner
  </p>
  <div class="prose-kitchen text-sm">Der volle Komfort entsteht mit einem Dokumentenscanner. Richte auf dem Server eine
Netzwerkfreigabe (z. B. Samba) ein, die auf <code>~/paperless/consume</code> zeigt, und stelle deinen
Scanner so ein, dass er dorthin scannt. Ab dann gilt: Blatt einlegen, Knopf drücken – wenige
Sekunden später ist das Dokument erkannt, verschlagwortet und durchsuchbar im Archiv. Für
unterwegs gibt es zudem Community-Apps (z. B. „Paperless Mobile&quot;), die sich mit deiner
Instanz verbinden.</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>Deine Unterlagen hängen im Internet
  </p>
  <div class="prose-kitchen text-sm">In Paperless liegen sensible Dokumente – Rechnungen, Verträge, Behördenpost. Sobald die
Instanz über <code>paperless.DEINE_DOMAIN</code> erreichbar ist, steht die Anmeldeseite offen im Netz.
Vergib deshalb ein <strong>langes, einmaliges Passwort</strong>, und aktiviere unter <strong>Einstellungen</strong> die
<strong>Zwei-Faktor-Authentifizierung</strong>. Wer maximale Sicherheit will, macht Paperless nur über ein
VPN erreichbar – für ein reines Privatarchiv, das nur du nutzt, ist das eine Überlegung wert.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> Beim Login erscheint „<strong>Forbidden (403)</strong>&quot; oder „CSRF verification failed&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> <code>PAPERLESS_URL</code> ist nicht oder falsch gesetzt. Sie muss exakt deiner
HTTPS-Adresse entsprechen (<code>https://paperless.DEINE_DOMAIN</code>, ohne Schrägstrich am Ende).
Nach der Korrektur <code>docker compose up -d</code>.</p>
<p><strong>Symptom:</strong> Dateien im Consume-Ordner werden nicht verarbeitet, „permission denied&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> Häufigste Ursache: Die Ordner wurden nicht vorab angelegt
(Schritt 3), sondern beim ersten Start vom Docker-Daemon erzeugt – dann gehören sie
<code>root</code>. Mit <code>sudo chown -R $(id -u):$(id -g) ~/paperless/consume ~/paperless/export</code>
gehören sie wieder dir. Ansonsten: <code>USERMAP_UID</code>/<code>USERMAP_GID</code> passen nicht zum
Besitzer des Ordners – ermittle deine Kennung mit <code>id -u</code> und <code>id -g</code>, trag die Werte
ein und starte neu.</p>
<p><strong>Symptom:</strong> Hochgeladene Dokumente bleiben „in Bearbeitung&quot; hängen.</p>
<p><strong>Ursache &amp; Lösung:</strong> Die Hintergrundverarbeitung läuft über Redis. Prüfe, dass der
<code>broker</code>-Container läuft, und sieh unter <strong>Dateiaufgaben</strong> nach der Fehlermeldung der
fehlgeschlagenen Aufgabe.</p>
<p><strong>Symptom:</strong> Die Texterkennung liefert Unsinn oder erkennt nichts.</p>
<p><strong>Ursache &amp; Lösung:</strong> Falsche OCR-Sprache. Setz <code>PAPERLESS_OCR_LANGUAGE=deu</code> (oder
<code>deu+eng</code> für gemischte Dokumente). Nur installierte Sprachen funktionieren.</p>
<p><strong>Symptom:</strong> Office-Dokumente (Word, Excel) werden nicht angenommen oder enden im Timeout.</p>
<p><strong>Ursache &amp; Lösung:</strong> Dafür sind Gotenberg und Tika zuständig. Prüfe, dass beide Container
laufen und <code>PAPERLESS_TIKA_ENABLED=1</code> samt der beiden Endpoint-Variablen gesetzt ist.</p>
<p><strong>Symptom:</strong> Beim Massenimport wird der Server sehr langsam, die CPU ist dauerhaft am Anschlag.</p>
<p><strong>Ursache &amp; Lösung:</strong> OCR ist rechenintensiv, und Paperless nutzt standardmäßig alle Kerne.
Auf kleinen Servern kannst du die Last drosseln, indem du die Zahl der Worker bzw. Threads
begrenzt (<code>PAPERLESS_TASK_WORKERS</code>, <code>PAPERLESS_THREADS_PER_WORKER</code>). Dann dauert der Import
länger, aber die Oberfläche bleibt bedienbar.</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li>
<p><strong>Das saubere Backup macht der <code>document_exporter</code>.</strong> Er schreibt alle Dokumente,
Vorschaubilder, Metadaten und den Datenbankinhalt in den <code>export</code>-Ordner – portabel und
wieder importierbar:</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> -T webserver document_exporter ../export</span></span></code></pre></div>
</div>
<p>Sichere den <code>export</code>-Ordner anschließend <strong>verschlüsselt und off-site</strong> mit
<a href="/tutorials/backups-mit-restic/">Restic</a>. Alternativ sicherst du die Volumes <code>media</code>,
<code>data</code> und die Datenbank direkt – der Exporter ist aber der empfohlene, umzugssichere Weg.</p>
</li>
<li>
<p><strong>Beim Wiederherstellen die gleiche Version verwenden.</strong> Ein Export enthält ein Abbild
passend zum Datenbank-Schema; spiel ihn nur in eine Paperless-Instanz <strong>derselben Version</strong>
ein (<code>document_importer ../export</code>).</p>
</li>
<li>
<p><strong>Updates.</strong> Vor dem Update ein Backup ziehen, dann den Image-Tag erhöhen (z. B.
<code>2.20.15</code> → nächste Version), <code>docker compose pull</code> und <code>docker compose up -d</code>. Die
Datenbank-Migrationen laufen beim Start automatisch. Bleib bei der stabilen <code>2.x</code>-Reihe –
die <code>3.0</code>-Beta ist noch nicht für den Produktivbetrieb gedacht.</p>
</li>
<li>
<p><strong>Backup automatisieren.</strong> Den <code>document_exporter</code> legst du am besten in einen täglichen
Cron-Job (z. B. nachts), der anschließend den <code>export</code>-Ordner per Restic sichert. So hast
du jeden Morgen einen frischen, wiederherstellbaren Stand – ohne daran denken zu müssen.</p>
</li>
<li>
<p><strong>Ehrlich zum Aufwand:</strong> Paperless läuft danach sehr wartungsarm. Der eigentliche Aufwand
ist das Einsortieren neuer Dokumente – das nimmt dir mit etwas eingerichteter Automatik
aber zunehmend die Software ab. Plane einmal die grobe Tag-Struktur, dann trägt sich das
Archiv weitgehend selbst.</p>
</li>
</ul>
]]></content:encoded></item><item><title>Nextcloud absichern &amp; optimieren: alle Warnungen abarbeiten (Teil 2)</title><link>https://serverkueche.de/tutorials/nextcloud-optimieren-absichern/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/nextcloud-optimieren-absichern/</guid><description>Die Admin-Übersicht deiner Nextcloud grün bekommen: HSTS-Header, E-Mail-Versand, Zwei-Faktor-Pflicht und Brute-Force-Schutz mit echten IPs einrichten.</description><content:encoded><![CDATA[<p>Deine Nextcloud läuft – aber in der Admin-Übersicht leuchten gelbe Warnungen, es gibt
keine funktionierende E-Mail und kein zweiter Faktor schützt deine Konten. In diesem
zweiten Teil machen wir aus „läuft&quot; ein „sauber abgesichert und flott&quot;: Wir arbeiten
jede Warnung ab, bis die Übersicht grün ist.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende zeigt die <strong>Sicherheits- &amp; Einrichtungsübersicht</strong> deiner Nextcloud keine
Warnungen mehr. Konkret bauen wir auf der Installation aus
<a href="/tutorials/nextcloud-eigene-cloud/">Teil 1</a> auf und ergänzen: den fehlenden
<strong>HSTS-Sicherheitsheader</strong> über Traefik, ein konfiguriertes <strong>Wartungsfenster</strong>, den
funktionierenden <strong>E-Mail-Versand</strong> (für Passwort-Resets und Benachrichtigungen), eine
<strong>erzwungene Zwei-Faktor-Authentifizierung</strong> und einen <strong>Brute-Force-Schutz, der die
echten Angreifer-IPs sieht</strong> – die Grundlage für Fail2ban.</p>
<p>Getestet ist alles gegen <strong>Nextcloud 34.0.1</strong> hinter <strong>Traefik v3.7</strong> auf Debian 13 mit
PHP 8.5. Das ist Fleißarbeit, aber genau die Fleißarbeit, die in 90 % aller
Nextcloud-Anleitungen fehlt – und die den Unterschied zwischen „irgendwie online&quot; und
„vernünftig betrieben&quot; ausmacht.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Eine <strong>laufende Nextcloud</strong> wie in
<a href="/tutorials/nextcloud-eigene-cloud/">Nextcloud selbst hosten (Teil 1)</a> aufgesetzt:
klassisches <code>nextcloud</code>-Image hinter Traefik, mit MariaDB, Redis und Cron-Sidecar.
Die Container-Namen aus Teil 1 (<code>nc-app</code>, <code>nc-db</code>, <code>nc-redis</code>, <code>nc-cron</code>) und der
Compose-Ordner <code>~/nextcloud</code> sind hier die Basis.</li>
<li><strong>SSH-Zugriff</strong> auf den Server und das Kommandozeilen-Werkzeug <strong><code>occ</code></strong> – wir rufen es
wie in Teil 1 im App-Container auf: <code>docker exec -u www-data nc-app php occ &lt;befehl&gt;</code>.</li>
<li>Ein <strong>Traefik-Reverse-Proxy</strong> mit dem <code>proxy</code>-Netz und dem Resolver <code>le</code> wie im
Tutorial <a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a> – die
Sicherheits-Header hängen wir als Traefik-Middleware an.</li>
</ul>
<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>occ im Griff behalten
  </p>
  <div class="prose-kitchen text-sm"><p><code>occ</code> ist Nextclouds Admin-Werkzeug. Weil die Container in Teil 1 feste Namen bekommen
haben (<code>container_name</code>), sprichst du den App-Container direkt an – egal, in welchem
Verzeichnis du bist:</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 <span class="nb">exec</span> -u www-data nc-app php occ status</span></span></code></pre></div>
</div>
<p>Du solltest <code>installed: true</code> und deine Version sehen. Diesen Präfix
(<code>docker exec -u www-data nc-app php occ …</code>) kürzen wir im Folgenden nicht ab – so kannst
du jeden Befehl direkt kopieren. Nur die wenigen <code>docker compose …</code>-Befehle (etwa der
Container-Neustart in Schritt 3) führst du weiterhin aus dem Ordner <code>~/nextcloud</code> aus.</p>
</div>
</div>
<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-die-admin-übersicht-als-bestandsaufnahme-lesen">Schritt 1: Die Admin-Übersicht als Bestandsaufnahme lesen</h3>
<p>Melde dich als Administrator an und öffne <strong>Administrationseinstellungen → Übersicht</strong>.
Oben steht der Abschnitt <strong>„Sicherheits- &amp; Einrichtungswarnungen&quot;</strong>. Direkt nach einer
Standardinstallation sieht das so aus:</p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-optimieren-absichern/nc-uebersicht-warnungen_hu_ded0bba1c144426e.webp" srcset="/tutorials/nextcloud-optimieren-absichern/nc-uebersicht-warnungen_hu_a4ebfe3ed3a25ade.webp 480w, /tutorials/nextcloud-optimieren-absichern/nc-uebersicht-warnungen_hu_ded0bba1c144426e.webp 768w, /tutorials/nextcloud-optimieren-absichern/nc-uebersicht-warnungen_hu_bbc66bad38685276.webp 1200w, /tutorials/nextcloud-optimieren-absichern/nc-uebersicht-warnungen_hu_2de722e751877ff1.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-optimieren-absichern/nc-uebersicht-warnungen_hu_9a42eb97ca12bc11.webp"
    alt="Die Nextcloud-Admin-Übersicht listet mehrere gelbe Warnungen zu Wartungsfenster, MIME-Type-Migrationen und HTTP-Headern auf" title="Ausgangslage: drei Warnungen und mehrere Hinweise in der Admin-Übersicht"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Ausgangslage: drei Warnungen und mehrere Hinweise in der Admin-Übersicht</figcaption></figure></p>
<p>Nextcloud unterscheidet drei Stufen: <strong>gelbe Warnungen</strong> (⚠, solltest du beheben),
<strong>blaue Hinweise</strong> (ℹ, meist optional) und stillschweigend bestandene Prüfungen. Bei
unserer frischen Instanz stehen drei Warnungen und ein paar Hinweise an:</p>
<ul>
<li>⚠ <strong>Beginn des Wartungsfensters</strong> ist nicht gesetzt (Schritt 2)</li>
<li>⚠ <strong>MIME-Type-Migrationen verfügbar</strong> (Schritt 2)</li>
<li>⚠ <strong>HTTP-Header</strong> – der <code>Strict-Transport-Security</code>-Header fehlt (Schritt 3)</li>
<li>ℹ <strong>E-Mail-Test</strong> – noch kein Mailserver hinterlegt (Schritt 4)</li>
<li>ℹ <strong>Zwei-Faktor-Konfiguration</strong> – 2FA ist verfügbar, aber nicht verpflichtend (Schritt 5)</li>
</ul>
<p>Denselben Bericht bekommst du auch auf der Kommandozeile – praktisch, um den Fortschritt
zwischendurch zu prüfen, ohne durch die Weboberfläche zu klicken:</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 <span class="nb">exec</span> -u www-data nc-app php occ setupchecks</span></span></code></pre></div>
</div>
<p>Der Befehl gibt jede Prüfung mit ✓, ⚠ oder ✗ aus. Genau diese Liste arbeiten wir jetzt
von oben nach unten ab.</p>
<h3 id="schritt-2-die-zwei-schnellen-warnungen-beheben">Schritt 2: Die zwei schnellen Warnungen beheben</h3>
<p>Zwei Warnungen sind mit je einem Befehl erledigt.</p>
<p><strong>Wartungsfenster festlegen.</strong> Nextcloud führt einmal täglich rechenintensive
Aufräumjobs aus (Vorschauen, Aktivitäten, Kalender-Wiederholungen). Ohne definiertes
Zeitfenster laufen die <em>irgendwann</em> – auch mitten am Tag. Leg einen Startzeitpunkt fest,
zu dem kaum jemand arbeitet. Der Wert ist eine <strong>volle Stunde in UTC</strong>; <code>1</code> legt den
Beginn auf 01:00 UTC – der Check meldet danach das Fenster von 1:00 bis 7:00 UTC:</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 <span class="nb">exec</span> -u www-data nc-app php occ config:system:set maintenance_window_start --type<span class="o">=</span>integer --value<span class="o">=</span><span class="m">1</span></span></span></code></pre></div>
</div>
<p>Zur Kontrolle bestätigt der Setup-Check danach:</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">✓ Maintenance window start: Maintenance window to execute heavy background jobs is between 1:00 UTC and 7:00 UTC</span></span></code></pre></div>
</div>
<p><strong>MIME-Type-Migrationen ausführen.</strong> Gelegentlich lernt Nextcloud neue Dateitypen (etwa
für bessere Icons und Vorschauen). Diese Migration läuft bei Updates <em>nicht</em> automatisch,
weil sie auf großen Instanzen dauern kann. Auf einer frischen Cloud ist sie in Sekunden
durch:</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 <span class="nb">exec</span> -u www-data nc-app php occ maintenance:repair --include-expensive</span></span></code></pre></div>
</div>
<p>Du siehst eine Liste abgearbeiteter Reparaturschritte (u. a. <code>Repair mime types</code>).
Danach meldet der Check <code>Mimetype migrations available: None</code>. Zwei Warnungen weniger.</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>Und die blauen Hinweise?
  </p>
  <div class="prose-kitchen text-sm">Die Hinweise <strong>„AppAPI-Bereitstellungs-Daemon&quot;</strong> und <strong>„Konfiguration Server-ID&quot;</strong> kannst
du auf einer normalen Ein-Server-Installation ignorieren. Der erste betrifft nur die
Installation externer „Ex-Apps&quot; über einen Docker-Daemon, der zweite nur Setups, die über
mehrere PHP-Server verteilt laufen. Beides brauchst du hier nicht – deshalb bleiben es
Hinweise und keine Warnungen.</div>
</div>
<h3 id="schritt-3-den-hsts-sicherheitsheader-über-traefik-setzen">Schritt 3: Den HSTS-Sicherheitsheader über Traefik setzen</h3>
<p>Die dritte Warnung ist die wichtigste: Der <strong><code>Strict-Transport-Security</code>-Header</strong> (HSTS)
fehlt. Er weist den Browser an, diese Domain <strong>ausschließlich über HTTPS</strong> anzusprechen –
auch dann, wenn jemand <code>http://</code> eintippt oder ein Angreifer die Verbindung umzubiegen
versucht. Ohne HSTS bleibt ein kleines Zeitfenster für Downgrade-Angriffe offen.</p>
<p>Weil unsere Nextcloud hinter Traefik sitzt, setzen wir den Header <strong>im Proxy</strong>, nicht in
Nextcloud – dort gehört er hin, denn Traefik terminiert das TLS. Ergänze in der
<code>compose.yaml</code> deiner Nextcloud (aus Teil 1) die Middleware <code>nc-secure</code> und häng sie an
den Router. Konkret ändern sich zwei Stellen im <code>labels</code>-Block von <code>nc-app</code>.</p>
<p>Erstens die Router-Zeile um <code>nc-secure</code> erweitern:</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="s2">&#34;traefik.http.routers.nc.middlewares=nc-dav,nc-secure&#34;</span></span></span></code></pre></div>
</div>
<p>Zweitens die Header-Middleware definieren (direkt bei den anderen <code>nc-app</code>-Labels):</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="s2">&#34;traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.nc-secure.headers.stsIncludeSubdomains=true&#34;</span></span></span></code></pre></div>
</div>
<p><code>stsSeconds=15552000</code> sind 180 Tage – der von Nextcloud geforderte Mindestwert. Übernimm
die Änderung und lade nur den App-Container neu:</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 nc-app</span></span></code></pre></div>
</div>
<p>Prüfe nach ein paar Sekunden von deinem eigenen Rechner aus, dass der Header wirklich
ausgeliefert wird:</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 -sS -I https://cloud.DEINE_DOMAIN/ <span class="p">|</span> grep -i strict-transport</span></span></code></pre></div>
</div>
<p>Du solltest genau das sehen:</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">strict-transport-security: max-age=15552000; includeSubDomains</span></span></code></pre></div>
</div>
<p>In der Admin-Übersicht wird die HTTP-Header-Warnung damit grün: <em>„Your server is correctly
configured to send security headers.&quot;</em> Die übrigen Header (<code>X-Content-Type-Options</code>,
<code>X-Frame-Options</code>, <code>Referrer-Policy</code> …) liefert Nextcloud selbst aus – nur HSTS musste vom
Proxy kommen.</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>preload ist eine Einbahnstraße
  </p>
  <div class="prose-kitchen text-sm"><code>stsIncludeSubdomains=true</code> erweitert den HTTPS-Zwang auf alle Subdomains <strong>von
<code>cloud.DEINE_DOMAIN</code></strong> (nicht auf Geschwister wie <code>www.DEINE_DOMAIN</code> – der Header gilt
immer nur für den Host, der ihn ausliefert). Bewusst <strong>nicht</strong> gesetzt ist
<code>stsPreload=true</code>: Damit würdest du signalisieren, dass du die Domain in die
<a href="https://hstspreload.org/">HSTS-Preload-Liste</a> der Browser eintragen lassen willst – und
so ein Eintrag ist nur mit wochenlangem Vorlauf wieder rückgängig zu machen. Falls du
das eines Tages bewusst willst: Die Preload-Liste akzeptiert nur <code>max-age</code> ≥ 31536000
(1 Jahr) – mit den 15552000 oben wäre das Flag ohnehin wirkungslos. Setze <code>preload</code>
nur, wenn wirklich <em>jeder</em> Dienst unter der Domain dauerhaft HTTPS spricht; der Header
ist auch ohne vollwertig.</div>
</div>
<h3 id="schritt-4-e-mail-versand-einrichten">Schritt 4: E-Mail-Versand einrichten</h3>
<p>Ohne funktionierenden Mailversand kann Nextcloud keine <strong>Passwort-Resets</strong> verschicken,
keine <strong>Freigabe-Benachrichtigungen</strong> und keine <strong>Sicherheitswarnungen</strong> – und der
Hinweis „E-Mail-Test&quot; bleibt stehen. Das holen wir nach.</p>
<p>Öffne <strong>Administrationseinstellungen → Grundeinstellungen</strong> und scroll zum Abschnitt
<strong>„E-Mail-Server&quot;</strong>. Trag die Zugangsdaten deines Mail-Anbieters ein:</p>
<ul>
<li><strong>Sendemodus:</strong> <code>SMTP</code></li>
<li><strong>Verschlüsselung:</strong> <code>SSL/TLS</code> (Port <strong>465</strong>) oder <code>STARTTLS</code> (Port <strong>587</strong>) – nie
unverschlüsselt</li>
<li><strong>Absenderadresse:</strong> z. B. <code>cloud</code> @ <code>DEINE_DOMAIN</code></li>
<li><strong>Serveradresse:</strong> Host und Port deines Anbieters</li>
<li><strong>Authentifizierung:</strong> einschalten und Benutzername/Passwort hinterlegen</li>
</ul>
<p>Trag oben unter <strong>Persönliche Informationen</strong> eine E-Mail-Adresse für dein Admin-Konto
ein (dorthin geht die Testmail). Dann klick auf <strong>„Test-E-Mail senden&quot;</strong>. Kommt die Mail
an, bestätigt Nextcloud das mit einer grünen Meldung:</p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-optimieren-absichern/nc-email-test_hu_f1a5ed435fa3db91.webp" srcset="/tutorials/nextcloud-optimieren-absichern/nc-email-test_hu_5adf7017629a931a.webp 480w, /tutorials/nextcloud-optimieren-absichern/nc-email-test_hu_f1a5ed435fa3db91.webp 768w, /tutorials/nextcloud-optimieren-absichern/nc-email-test_hu_5646fac37d8678c9.webp 1200w, /tutorials/nextcloud-optimieren-absichern/nc-email-test_hu_4e4b07dca9cf3d75.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-optimieren-absichern/nc-email-test_hu_39bdda64e2c97535.webp"
    alt="Der E-Mail-Server ist mit SMTP konfiguriert, ein Test-Versand wird oben rechts mit „E-Mail wurde versandt&quot; bestätigt" title="Erfolgreicher Test-Versand – die Werte im Bild stammen aus einem internen Test-Mailserver und sind NICHT für echte Postfächer zu übernehmen; dort gehören Host, Port 587/465 mit STARTTLS/SSL und die Zugangsdaten deines Anbieters hin"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Erfolgreicher Test-Versand – die Werte im Bild stammen aus einem internen Test-Mailserver und sind NICHT für echte Postfächer zu übernehmen; dort gehören Host, Port 587/465 mit STARTTLS/SSL und die Zugangsdaten deines Anbieters hin</figcaption></figure></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>App-Passwort statt Kontopasswort
  </p>
  <div class="prose-kitchen text-sm">Nutzt dein Mail-Anbieter selbst Zwei-Faktor-Authentifizierung (Gmail, Mailbox.org, viele
andere), funktioniert dein normales Login-Passwort hier <strong>nicht</strong>. Leg im Konto deines
Anbieters ein dediziertes <strong>App-Passwort</strong> an und trag das ein. Das lässt sich zudem
gezielt widerrufen, ohne dein Haupt-Passwort zu ändern.</div>
</div>
<h3 id="schritt-5-zwei-faktor-authentifizierung-erzwingen">Schritt 5: Zwei-Faktor-Authentifizierung erzwingen</h3>
<p>Ein gestohlenes Passwort ist der häufigste Weg, wie Konten übernommen werden. Ein
<strong>zweiter Faktor</strong> (ein zeitbasierter Code aus einer Authenticator-App, TOTP) macht das
Passwort allein wertlos. Nextcloud bringt den passenden Anbieter schon mit – wir müssen
ihn nur aktivieren und zur Pflicht machen.</p>
<p>Aktiviere zunächst die TOTP-App (sie ist im Lieferumfang enthalten):</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 <span class="nb">exec</span> -u www-data nc-app php occ app:enable twofactor_totp</span></span></code></pre></div>
</div>
<p>Jeder Nutzer richtet seinen zweiten Faktor danach selbst ein – unter <strong>Persönliche
Einstellungen → Sicherheit → „TOTP (Authenticator-App)&quot;</strong>. Beim Aktivieren zeigt
Nextcloud einen QR-Code, den man mit einer App wie Aegis, andOTP oder Google Authenticator
scannt. Zusätzlich empfiehlt sich die App <strong>„Zwei-Faktor-Backup-Codes&quot;</strong> – die einmaligen
Codes retten dich, wenn das Handy verloren geht.</p>
<p>Damit niemand die Absicherung „vergisst&quot;, erzwingst du 2FA für die Administratoren. Im Web
findest du das unter <strong>Administrationseinstellungen → Sicherheit → „Zwei-Faktor-
Authentifizierung erzwingen&quot;</strong>, per Kommandozeile geht es so:</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 <span class="nb">exec</span> -u www-data nc-app php occ twofactorauth:enforce --on --group<span class="o">=</span>admin</span></span></code></pre></div>
</div>
<p><figure class="my-6"><img src="/tutorials/nextcloud-optimieren-absichern/nc-zweifaktor_hu_a132ce2453d02b15.webp" srcset="/tutorials/nextcloud-optimieren-absichern/nc-zweifaktor_hu_94f90414c751c23b.webp 480w, /tutorials/nextcloud-optimieren-absichern/nc-zweifaktor_hu_a132ce2453d02b15.webp 768w, /tutorials/nextcloud-optimieren-absichern/nc-zweifaktor_hu_34a2ce03c7d7cb52.webp 1200w, /tutorials/nextcloud-optimieren-absichern/nc-zweifaktor_hu_6beccff3d885822a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-optimieren-absichern/nc-zweifaktor_hu_21452c4f02d2cd91.webp"
    alt="Die Sicherheits-Einstellungen zeigen die erzwungene Zwei-Faktor-Authentifizierung für die Gruppe „admin“ sowie den Brute-Force-Status mit korrekt erkannter Client-IP" title="Zwei-Faktor-Pflicht für die Gruppe „admin“ – und darüber der Beleg, dass Nextcloud die echte Client-IP erkennt"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Zwei-Faktor-Pflicht für die Gruppe „admin“ – und darüber der Beleg, dass Nextcloud die echte Client-IP erkennt</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>Sperr dich nicht selbst aus
  </p>
  <div class="prose-kitchen text-sm">Sobald die Erzwingung aktiv ist, <strong>muss</strong> jedes Mitglied der Gruppe beim nächsten Login
einen zweiten Faktor einrichten. Richte deshalb <strong>zuerst deinen eigenen</strong> zweiten Faktor
ein und bewahre die Backup-Codes sicher auf, bevor du die Pflicht scharf schaltest. Falls
doch etwas schiefgeht, hebst du die Erzwingung über die Kommandozeile wieder auf:
<code>docker exec -u www-data nc-app php occ twofactorauth:enforce --off</code>. Einen
einzelnen festgefahrenen Faktor entfernst du mit
<code>… php occ twofactorauth:disable BENUTZER totp</code>.</div>
</div>
<h3 id="schritt-6-brute-force-schutz-mit-echten-ips--basis-für-fail2ban">Schritt 6: Brute-Force-Schutz mit echten IPs – Basis für Fail2ban</h3>
<p>Nextcloud drosselt fehlgeschlagene Logins von Haus aus: Nach mehreren Fehlversuchen
verzögert es weitere Anfragen <strong>derselben IP</strong>. Dieser Schutz steht und fällt aber damit,
dass Nextcloud die <strong>echte Client-IP</strong> sieht – und nicht die interne IP von Traefik. Genau
das leistet die <code>TRUSTED_PROXIES</code>-Einstellung aus Teil 1.</p>
<p>Ob es funktioniert, siehst du unter <strong>Administrationseinstellungen → Sicherheit</strong> im
Kasten „Brute-Force-Zulassungsliste&quot; (im Screenshot oben). Dort steht deine aktuelle,
korrekt erkannte öffentliche IP:</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">Ihre aktuelle IP-Adresse wird als &#34;DEINE_IP&#34; erkannt. Diese Adresse wird derzeit nicht gedrosselt.</span></span></code></pre></div>
</div>
<p>Steht da stattdessen eine <code>172.x.x.x</code>-Adresse aus dem Docker-Netz, greift <code>TRUSTED_PROXIES</code>
nicht – dann würde der Brute-Force-Schutz alle Nutzer über einen Kamm scheren und im
Zweifel Traefik selbst sperren. In dem Fall zurück zu Teil 1, Schritt 2/3.</p>
<p><strong>Harte Sperren mit Fail2ban.</strong> Die eingebaute Drosselung verzögert nur; wer Angreifer
richtig aussperren will, kombiniert Nextcloud mit
<a href="/tutorials/fail2ban-einrichten/">Fail2ban</a>. Nextcloud protokolliert jeden Fehlversuch im
JSON-Log – und dank <code>TRUSTED_PROXIES</code> mit der echten IP:</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;level&#34;:2,&#34;time&#34;:&#34;2026-07-18T08:14:05+00:00&#34;,&#34;remoteAddr&#34;:&#34;203.0.113.47&#34;,&#34;user&#34;:&#34;--&#34;,&#34;app&#34;:&#34;no app in context&#34;,&#34;method&#34;:&#34;POST&#34;,&#34;url&#34;:&#34;/login&#34;,&#34;message&#34;:&#34;Login failed: BENUTZER (Remote IP: 203.0.113.47)&#34;}</span></span></code></pre></div>
</div>
<p>Dieses Log liegt im Nextcloud-Volume. Den Pfad auf dem Host findest du so:</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 inspect -f <span class="s1">&#39;{{ range .Mounts }}{{ if eq .Destination &#34;/var/www/html&#34; }}{{ .Source }}{{ end }}{{ end }}&#39;</span> nc-app</span></span></code></pre></div>
</div>
<p>Darunter liegt die Datei <code>data/nextcloud.log</code>. Auf die zeigst du einen Fail2ban-Filter,
der auf <code>remoteAddr</code> matcht, plus ein Jail nach dem Muster aus dem
<a href="/tutorials/fail2ban-einrichten/">Fail2ban-Tutorial</a>. So wandert nach ein paar
Fehlversuchen die Angreifer-IP direkt in die Firewall – bevor die Anfrage Nextcloud
überhaupt erreicht.</p>
<h3 id="schritt-7-caching--php-prüfen--das-performance-fundament">Schritt 7: Caching &amp; PHP prüfen – das Performance-Fundament</h3>
<p>Zum Schluss vergewissern wir uns, dass die Performance-Basis steht. Vieles davon hat Teil 1
über die <code>REDIS_HOST</code>-Variable schon automatisch eingerichtet – prüfen schadet trotzdem
nicht, denn genau hier wird eine Nextcloud zäh oder flott.</p>
<p>Nextcloud nutzt <strong>drei</strong> Cache-Rollen. Kontrolliere alle drei:</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 <span class="nb">exec</span> -u www-data nc-app php occ config:system:get memcache.local
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:get memcache.distributed
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:get memcache.locking</span></span></code></pre></div>
</div>
<p>Erwartet:</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">\OC\Memcache\APCu
</span></span><span class="line"><span class="cl">\OC\Memcache\Redis
</span></span><span class="line"><span class="cl">\OC\Memcache\Redis</span></span></code></pre></div>
</div>
<ul>
<li><strong>Lokaler Cache (APCu):</strong> hält häufig genutzte Daten im RAM des PHP-Prozesses – der
spürbarste Beschleuniger im Alltag.</li>
<li><strong>Verteilter Cache (Redis):</strong> teilt Cache-Daten über Prozesse hinweg, wichtig sobald
App- und Cron-Container zusammenspielen.</li>
<li><strong>File-Locking (Redis):</strong> verhindert, dass zwei Zugriffe dieselbe Datei gleichzeitig
verändern und sie beschädigen. Der Grund, warum wir in Teil 1 überhaupt Redis eingebaut
haben.</li>
</ul>
<p>Passt das Zusammenspiel, meldet <code>docker exec -u www-data nc-app php occ setupchecks</code> die
Zeile <code>✓ Memcache: Configured</code>. (Bestandene Prüfungen zeigt die Web-Übersicht nicht an –
dort tauchen nur Warnungen und Hinweise auf, deshalb kontrollierst du das hier per CLI.)</p>
<p><strong>OPcache</strong> – der Bytecode-Cache von PHP – ist im offiziellen <code>nextcloud</code>-Image bereits
sinnvoll vorkonfiguriert (u. a. <code>opcache.interned_strings_buffer=32</code>). Du musst hier also
nichts nachziehen; die früher berüchtigte OPcache-Warnung taucht mit aktuellen Images gar
nicht mehr auf. Nachschauen kannst du bei Bedarf so:</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 <span class="nb">exec</span> -u www-data nc-app php -i <span class="p">|</span> grep opcache.interned_strings_buffer</span></span></code></pre></div>
</div>
<p>Damit ist die Übersicht warnungsfrei:</p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-optimieren-absichern/nc-uebersicht-gruen_hu_e398290706625ab9.webp" srcset="/tutorials/nextcloud-optimieren-absichern/nc-uebersicht-gruen_hu_e354f69e1443ecd8.webp 480w, /tutorials/nextcloud-optimieren-absichern/nc-uebersicht-gruen_hu_e398290706625ab9.webp 768w, /tutorials/nextcloud-optimieren-absichern/nc-uebersicht-gruen_hu_d2ede7a8e25e526d.webp 1200w, /tutorials/nextcloud-optimieren-absichern/nc-uebersicht-gruen_hu_77659c13bfe90ad3.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-optimieren-absichern/nc-uebersicht-gruen_hu_c26ff36bf5d6b604.webp"
    alt="Die Nextcloud-Admin-Übersicht zeigt keine gelben Warnungen mehr, nur noch zwei optionale Hinweise, und meldet die aktuelle Version" title="Ergebnis: keine Warnungen mehr, Version aktuell – nur die zwei optionalen Hinweise bleiben"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Ergebnis: keine Warnungen mehr, Version aktuell – nur die zwei optionalen Hinweise bleiben</figcaption></figure></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>Vorschaubilder gezielt erzeugen
  </p>
  <div class="prose-kitchen text-sm">Wenn deine Nutzer viele Fotos ablegen, lohnt sich die App <strong>„Preview Generator&quot;</strong>: Ein
<code>occ</code>-Cronjob erzeugt Vorschaubilder im Voraus, statt sie beim ersten Öffnen langsam on
the fly zu berechnen. Ergänzt du sie, begrenze über
<code>occ config:system:set preview_max_x --value=2048</code> (und <code>preview_max_y</code>) die maximale
Vorschaugröße, sonst wächst der Speicherbedarf schnell.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> Nach dem Traefik-Neustart erscheint der HSTS-Header nicht in der <code>curl</code>-Ausgabe.</p>
<p><strong>Ursache &amp; Lösung:</strong> Meist ist die Middleware nicht am Router. Prüfe, dass die Zeile
<code>traefik.http.routers.nc.middlewares</code> <strong>beide</strong> Middlewares nennt (<code>nc-dav,nc-secure</code>) und
dass die drei <code>nc-secure</code>-Header-Labels im <code>nc-app</code>-Block stehen. Danach
<code>docker compose up -d nc-app</code>. Traefik übernimmt geänderte Labels erst beim Neu-Erstellen
des Containers.</p>
<p><strong>Symptom:</strong> Der E-Mail-Test schlägt fehl („Es gab ein Problem beim Senden der E-Mail&quot;).</p>
<p><strong>Ursache &amp; Lösung:</strong> Fast immer Port/Verschlüsselung oder Authentifizierung. Kombiniere
<code>SSL/TLS</code> mit Port <strong>465</strong> oder <code>STARTTLS</code> mit Port <strong>587</strong> – nicht kreuzweise. Nutzt dein
Anbieter 2FA, brauchst du ein <strong>App-Passwort</strong> (siehe Schritt 4). Details stehen im
Nextcloud-Log: <code>docker exec -u www-data nc-app php occ log:tail 20</code>.</p>
<p><strong>Symptom:</strong> Du kommst nach dem Erzwingen von 2FA selbst nicht mehr rein.</p>
<p><strong>Ursache &amp; Lösung:</strong> Du hattest noch keinen zweiten Faktor eingerichtet. Hebe die Pflicht
per Kommandozeile auf, richte deinen Faktor in Ruhe ein und schalte sie danach wieder
scharf: <code>docker exec -u www-data nc-app php occ twofactorauth:enforce --off</code>.</p>
<p><strong>Symptom:</strong> Im Brute-Force-Kasten steht eine <code>172.x.x.x</code>-Adresse statt deiner echten IP.</p>
<p><strong>Ursache &amp; Lösung:</strong> <code>TRUSTED_PROXIES</code> passt nicht zum tatsächlichen <code>proxy</code>-Subnetz.
Ermittle es mit <code>docker network inspect proxy -f '{{(index .IPAM.Config 0).Subnet}}'</code> und
trag den Wert in der <code>nc-app</code>-Umgebung ein (Teil 1, Schritt 2). Sonst sperrt der
Brute-Force-Schutz im Ernstfall den Proxy und damit alle Nutzer.</p>
<p><strong>Symptom:</strong> Nach einem Reboot meldet Nextcloud „Redis went away&quot; oder wird sehr langsam.</p>
<p><strong>Ursache &amp; Lösung:</strong> Der App-Container ist vor Redis gestartet. Stelle sicher, dass
<code>nc-redis</code> in <code>depends_on</code> steht (Teil 1) und mit <code>restart: unless-stopped</code> läuft – dann
fängt Docker den Startreihenfolge-Fall selbst ab.</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li><strong>Übersicht nach jedem Update prüfen.</strong> Neue Nextcloud-Versionen bringen neue Checks
mit. Wirf nach jedem Upgrade einen Blick auf <strong>Administrationseinstellungen → Übersicht</strong>
oder lass <code>occ setupchecks</code> laufen – so fängst du neue Warnungen früh ab.</li>
<li><strong>Regelmäßig den offiziellen Security-Scan nutzen.</strong> Der
<a href="https://scan.nextcloud.com/">Nextcloud Security Scan</a> prüft deine Domain von außen und
vergibt eine Note. Mit HSTS und aktuellen Versionen ist <strong>A/A+</strong> das realistische Ziel;
fällt die Note, ist meist ein Update überfällig.</li>
<li><strong>Zweite Faktoren im Blick behalten.</strong> Bewahre die Backup-Codes getrennt vom Handy auf.
Verliert ein Nutzer sein Gerät, entfernst du seinen Faktor mit
<code>occ twofactorauth:disable BENUTZER totp</code>, danach richtet er ihn neu ein.</li>
<li><strong>App-Passwörter aufräumen.</strong> Jeder verbundene Client (Desktop, Handy) bekommt ein
eigenes App-Passwort. Unter <strong>Persönliche Einstellungen → Sicherheit</strong> siehst du alle
Geräte und kannst einzelne bei Verlust gezielt widerrufen.</li>
<li><strong>Die Absicherung ersetzt kein Backup.</strong> All das schützt vor fremdem Zugriff, nicht vor
Datenverlust. Die konsistente Sicherung aus Datenbank-Dump und Datenverzeichnis bleibt
Pflicht – siehe den Wartungsabschnitt in <a href="/tutorials/nextcloud-eigene-cloud/">Teil 1</a> und
<a href="/tutorials/backups-mit-restic/">Backups mit Restic</a>. Ehrlich zum Aufwand: Plane den
monatlichen Update-Durchgang mit einem kurzen Blick auf die Übersicht fest ein – dann
bleibt deine Cloud dauerhaft grün.</li>
</ul>
]]></content:encoded></item><item><title>Nextcloud selbst hosten: deine eigene Cloud hinter Traefik</title><link>https://serverkueche.de/tutorials/nextcloud-eigene-cloud/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/nextcloud-eigene-cloud/</guid><description>Nextcloud mit Docker hinter Traefik aufsetzen: eigene Cloud für Dateien, Kalender und Kontakte – mit MariaDB, Redis und automatischem HTTPS.</description><content:encoded><![CDATA[<p>Deine Dateien, dein Kalender, deine Kontakte – aber auf deinem Server statt bei einem
Cloud-Konzern. <strong>Nextcloud</strong> ist das Flaggschiff des Selfhostings: eine vollwertige
Cloud, die du selbst betreibst. In diesem Rezept setzen wir sie sauber hinter Traefik
auf, mit eigener Datenbank, Caching und automatischem HTTPS.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Nextcloud 34</strong> hinter deinem Traefik-Proxy, erreichbar unter
<code>https://cloud.DEINE_DOMAIN</code> mit gültigem Let&rsquo;s-Encrypt-Zertifikat. Nextcloud ist deine
private Cloud: Dateien synchronisieren (wie bei Dropbox), Kalender und Kontakte über
alle Geräte, Fotos, Notizen, Office-Dokumente. Du verbindest den offiziellen
<strong>Desktop-Sync-Client</strong> und die <strong>Handy-Apps</strong> mit deinem Server – die Daten liegen
dabei ausschließlich bei dir.</p>
<p>Wir setzen bewusst auf das <strong>klassische <code>nextcloud</code>-Image</strong> (nicht auf „Nextcloud
All-in-One&quot;) plus <strong>MariaDB</strong> als Datenbank und <strong>Redis</strong> fürs Caching und File-Locking.
Diese Kombination ist stabil, gut dokumentiert und passt exakt in das Traefik-Muster aus
der Serverküche.</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 nicht Nextcloud AIO?
  </p>
  <div class="prose-kitchen text-sm">Nextcloud bietet auch ein „All-in-One&quot;-Paket (AIO) an. Das bringt aber sein <strong>eigenes
TLS und eigene Ports</strong> mit und will den Reverse-Proxy-Teil selbst übernehmen – das
beißt sich mit einem bestehenden Traefik. Für unser Setup ist das klassische Image die
richtige, kontrollierbare Wahl.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein laufender <strong>Traefik-Reverse-Proxy</strong> mit dem gemeinsamen <code>proxy</code>-Netzwerk und dem
Let&rsquo;s-Encrypt-Resolver <code>le</code> – wie im Tutorial
<a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a> aufgebaut.</li>
<li>Eine Subdomain <code>cloud.DEINE_DOMAIN</code>, deren DNS-Record (A/AAAA) auf deine Server-IP
zeigt – siehe <a href="/tutorials/domain-mit-server-verbinden/">Domain mit Server verbinden</a>.</li>
<li>Ein funktionierendes <strong>Backup</strong>. Eine Cloud ist der Ort, an dem Datenverlust am
meisten wehtut – richte zuerst <a href="/tutorials/backups-mit-restic/">Backups mit Restic</a>
ein.</li>
</ul>
<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>Wie groß muss der Server sein?
  </p>
  <div class="prose-kitchen text-sm">Nextcloud ist genügsamer als sein Ruf: Für den privaten Gebrauch oder eine kleine
Familie (1–5 Nutzer) reichen <strong>2 vCPU und 4 GB RAM</strong> gut aus – der getestete VPS 1000
passt. Der Flaschenhals ist der Arbeitsspeicher: Bei zu wenig RAM fängt der Server an zu
swappen und wird zäh. Redis (bauen wir gleich ein) entlastet spürbar. Für Nextcloud
Office/Collabora oder viele parallele Nutzer solltest du eher 8 GB einplanen.</div>
</div>
<p>Wie groß dein Server für deine Nutzerzahl sein sollte, schätzt dir der
<a href="/serverempfehlung/">Server-Rechner</a> in ein paar Klicks.</p>
<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">
  <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 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 10,36 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Für eine private Nextcloud mit ein paar Nutzern reicht der VPS 1000 gut aus.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12-12m?ref=44083" rel="sponsored noopener" target="_blank"
   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"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 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">
  <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-dns-record-anlegen">Schritt 1: DNS-Record anlegen</h3>
<p>Lege bei deinem DNS-Anbieter einen Eintrag <code>cloud.DEINE_DOMAIN</code> an, der auf deine
Server-IP zeigt (A-Record für IPv4, AAAA für IPv6). Prüfe, dass er aufgelöst wird:</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">dig +short cloud.DEINE_DOMAIN</span></span></code></pre></div>
</div>
<p>Du solltest deine Server-IP sehen. Erst wenn der Record steht, kann Traefik später das
HTTPS-Zertifikat holen.</p>
<h3 id="schritt-2-das-proxy-subnetz-ermitteln">Schritt 2: Das proxy-Subnetz ermitteln</h3>
<p>Nextcloud sitzt hinter Traefik. Damit Nextclouds Brute-Force-Schutz die <strong>echte</strong>
Besucher-IP erkennt (und nicht die interne Traefik-IP sperrt), müssen wir Traefik als
„vertrauenswürdigen Proxy&quot; eintragen. Dazu brauchst du das Subnetz deines
<code>proxy</code>-Netzwerks:</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 network inspect proxy -f <span class="s1">&#39;{{(index .IPAM.Config 0).Subnet}}&#39;</span></span></span></code></pre></div>
</div>
<p>Du bekommst etwas wie <code>172.18.0.0/16</code>. <strong>Merke dir diesen Wert</strong> – er kommt gleich als
<code>TRUSTED_PROXIES</code> in die Konfiguration.</p>
<h3 id="schritt-3-die-composeyaml-anlegen">Schritt 3: Die compose.yaml anlegen</h3>
<p>Leg einen Ordner an und wechsle hinein:</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 ~/nextcloud <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/nextcloud</span></span></code></pre></div>
</div>
<p>Erstelle die <code>compose.yaml</code>. Ersetze <code>cloud.DEINE_DOMAIN</code>, alle Passwörter und das
<code>TRUSTED_PROXIES</code>-Subnetz aus Schritt 2:</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="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">nc-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">mariadb:11.4</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">nc-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span>--<span class="l">transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW</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">MARIADB_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">EIN_STARKES_ROOT_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_USER</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">EIN_STARKES_DB_PASSWORT</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">nc_db:/var/lib/mysql</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&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;healthcheck.sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--connect&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--innodb_initialized&#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 class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc-redis</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">redis:8-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">nc-redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">redis-server --requirepass EIN_STARKES_REDIS_PASSWORT</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">default]</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc-app</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">nextcloud:34-apache</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">nc-app</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">nc-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">nc-redis</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_started</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">MYSQL_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">nc-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_USER</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">EIN_STARKES_DB_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REDIS_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">nc-redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REDIS_HOST_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">EIN_STARKES_REDIS_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NEXTCLOUD_TRUSTED_DOMAINS</span><span class="p">:</span><span class="w"> </span><span class="l">cloud.DEINE_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">OVERWRITEPROTOCOL</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">OVERWRITECLIURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://cloud.DEINE_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">TRUSTED_PROXIES</span><span class="p">:</span><span class="w"> </span><span class="m">172.18.0.0</span><span class="l">/16</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHP_MEMORY_LIMIT</span><span class="p">:</span><span class="w"> </span><span class="l">1024M</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHP_UPLOAD_LIMIT</span><span class="p">:</span><span class="w"> </span><span class="l">10G</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">nc_html:/var/www/html</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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.rule=Host(`cloud.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.middlewares=nc-dav&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.nc.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># .well-known-Redirect für CalDAV/CardDAV (siehe unten)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.nc-dav.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.nc-dav.redirectregex.replacement=https://cloud.DEINE_DOMAIN/remote.php/dav/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.nc-dav.redirectregex.permanent=true&#34;</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">default, proxy]</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></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">nc_html</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc_db</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">default</span><span class="p">:</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>Was hier wichtig ist:</p>
<ul>
<li><strong>Drei Container:</strong> <code>nc-app</code> (Nextcloud), <code>nc-db</code> (MariaDB), <code>nc-redis</code>. Nur <code>nc-app</code>
hängt im <strong><code>proxy</code>-Netz</strong> (für Traefik) <em>und</em> im internen <code>default</code>-Netz. Datenbank und
Redis bleiben ausschließlich intern – sie haben <strong>kein <code>ports:</code></strong> und sind von außen
nicht erreichbar.</li>
<li><strong>Zwei Named Volumes:</strong> <code>nc_html</code> (Programmcode, <code>config.php</code>, Nutzerdaten) und
<code>nc_db</code> (die MariaDB-Daten). Das <code>nc_db</code>-Volume ist Pflicht: Ohne den Eintrag landet
die Datenbank in einem <strong>anonymen</strong> Volume, und ein <code>docker compose down</code> gefolgt
von <code>up</code> würde sie <strong>unwiederbringlich verlieren</strong>.</li>
<li><strong><code>healthcheck</code> + <code>depends_on: service_healthy</code>:</strong> Nextcloud startet schneller als die
Datenbank initialisiert ist. Ohne diesen Healthcheck scheitert die Erstinstallation an
einem „Connection refused&quot;. So wartet der App-Container, bis MariaDB wirklich bereit ist.</li>
<li><strong><code>NEXTCLOUD_TRUSTED_DOMAINS</code>:</strong> ohne die eigene Domain hier begrüßt dich Nextcloud mit
„Zugriff über eine nicht vertrauenswürdige Domäne&quot;.</li>
<li><strong><code>OVERWRITEPROTOCOL: https</code></strong> sagt Nextcloud, dass es hinter HTTPS läuft – sonst baut
es interne Links als <code>http://</code> und Logins/Redirects brechen.</li>
<li><strong><code>TRUSTED_PROXIES</code>:</strong> das Subnetz aus Schritt 2. Damit erkennt Nextcloud die echte
Client-IP; sonst sperrt der Brute-Force-Schutz die Traefik-IP.</li>
<li><strong><code>PHP_MEMORY_LIMIT</code> / <code>PHP_UPLOAD_LIMIT</code>:</strong> die Defaults (512 MB) sind für große
Uploads zu klein. Hier großzügig gesetzt.</li>
<li><strong><code>loadbalancer.server.port=80</code>:</strong> das Apache-Image lauscht im Container auf Port 80.</li>
<li><strong>Die <code>nc-dav</code>-Middleware</strong> löst ein klassisches Nextcloud-Problem – dazu gleich mehr.</li>
</ul>
<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>CalDAV/CardDAV: der .well-known-Redirect
  </p>
  <div class="prose-kitchen text-sm">Nextcloud möchte Kalender-/Kontakt-Clients über <code>/.well-known/caldav</code> und
<code>/.well-known/carddav</code> zu <code>/remote.php/dav/</code> schicken. Hinter einem Reverse Proxy
funktioniert Nextclouds eigener Redirect nicht zuverlässig – das führt später zu einer
Warnung im Sicherheits-Check und zu Problemen bei der Kalender-Einrichtung. Die
<code>nc-dav</code>-Middleware oben erledigt den Redirect direkt in Traefik. Wichtig: Das Muster
muss die <strong>volle <code>https://…</code>-URL</strong> matchen – <code>redirectregex</code> prüft immer die komplette
URL, nicht nur den Pfad. Ein reines Pfad-Muster wie <code>^/.well-known/…</code> greift deshalb
nie.</div>
</div>
<h3 id="schritt-4-starten-und-den-erst-setup-durchlaufen">Schritt 4: Starten und den Erst-Setup durchlaufen</h3>
<p>Starte den Stack:</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></code></pre></div>
</div>
<p>Beim ersten Start initialisiert MariaDB die Datenbank und Nextcloud entpackt sich –
das dauert ein bis zwei Minuten. Verfolge es im Log:</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 logs -f nc-app</span></span></code></pre></div>
</div>
<p>Beim ersten Lauf entpackt das Image Nextcloud ins Volume; du siehst Zeilen wie
<code>Initializing nextcloud 34.0.1.2 ...</code> und schließlich <code>Initializing finished</code>, gefolgt
vom Apache-Start. Erscheint stattdessen wiederholt <code>restarting</code>, wirf einen Blick in den
Abschnitt „Wenn es nicht funktioniert&quot;. Mit <code>Strg+C</code> verlässt du die Log-Ansicht wieder
(der Container läuft weiter).</p>
<p>Ruf dann <code>https://cloud.DEINE_DOMAIN</code> auf. Traefik holt beim ersten Zugriff das
Zertifikat (kann ein paar Sekunden dauern). Weil wir Datenbank und Redis schon per
Umgebungsvariablen konfiguriert haben, erkennt Nextcloud das automatisch und fragt nur
noch nach einem <strong>Administrationskonto</strong>:</p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-eigene-cloud/nextcloud-setup_hu_bd390b1b06152f06.webp" srcset="/tutorials/nextcloud-eigene-cloud/nextcloud-setup_hu_54205e140ab4df51.webp 480w, /tutorials/nextcloud-eigene-cloud/nextcloud-setup_hu_bd390b1b06152f06.webp 768w, /tutorials/nextcloud-eigene-cloud/nextcloud-setup_hu_6aed3ce169effeae.webp 1200w, /tutorials/nextcloud-eigene-cloud/nextcloud-setup_hu_2dd5324525c2b322.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-eigene-cloud/nextcloud-setup_hu_b8b96096d363578.webp"
    alt="Der Nextcloud-Erst-Setup erkennt die Autoconfig und fragt nur noch nach Name und Passwort des Administrationskontos" title="Erst-Setup: Datenbank ist vorkonfiguriert, du legst nur das Admin-Konto an"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Erst-Setup: Datenbank ist vorkonfiguriert, du legst nur das Admin-Konto an</figcaption></figure></p>
<p>Vergib einen Admin-Namen und ein starkes Passwort und klick auf <strong>Installieren</strong>.
Nextcloud richtet die Instanz ein – danach landest du auf der Anmeldeseite:</p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-eigene-cloud/nextcloud-login_hu_80e3fea49ed0fa5d.webp" srcset="/tutorials/nextcloud-eigene-cloud/nextcloud-login_hu_7b6cf30b4bed90d8.webp 480w, /tutorials/nextcloud-eigene-cloud/nextcloud-login_hu_80e3fea49ed0fa5d.webp 768w, /tutorials/nextcloud-eigene-cloud/nextcloud-login_hu_31fcd7effc10218d.webp 1200w, /tutorials/nextcloud-eigene-cloud/nextcloud-login_hu_f9021634b2859d48.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-eigene-cloud/nextcloud-login_hu_76d7c24abc8fef7e.webp"
    alt="Die Nextcloud-Anmeldeseite unter der eigenen HTTPS-Domain" title="Die Anmeldeseite unter cloud.DEINE_DOMAIN – mit gültigem HTTPS von Traefik"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Anmeldeseite unter cloud.DEINE_DOMAIN – mit gültigem HTTPS von Traefik</figcaption></figure></p>
<p>Nach dem Login begrüßt dich das <strong>Dashboard</strong>, und unter <strong>Dateien</strong> findest du deine
Cloud mit ein paar Beispieldateien:</p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-eigene-cloud/nextcloud-dashboard_hu_c73d63376be11028.webp" srcset="/tutorials/nextcloud-eigene-cloud/nextcloud-dashboard_hu_8b8b0eae69ca9f5.webp 480w, /tutorials/nextcloud-eigene-cloud/nextcloud-dashboard_hu_c73d63376be11028.webp 768w, /tutorials/nextcloud-eigene-cloud/nextcloud-dashboard_hu_234eec6f42d11461.webp 1200w, /tutorials/nextcloud-eigene-cloud/nextcloud-dashboard_hu_74337e3e851cbd62.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-eigene-cloud/nextcloud-dashboard_hu_bb8653ade3e6bf20.webp"
    alt="Das Nextcloud-Dashboard mit dem Widget „Empfohlene Dateien&quot;" title="Das Dashboard nach dem ersten Login"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Dashboard nach dem ersten Login</figcaption></figure></p>
<p><figure class="my-6"><img src="/tutorials/nextcloud-eigene-cloud/nextcloud-dateien_hu_8c55170c4ab25cd1.webp" srcset="/tutorials/nextcloud-eigene-cloud/nextcloud-dateien_hu_b1b801f2689f8212.webp 480w, /tutorials/nextcloud-eigene-cloud/nextcloud-dateien_hu_8c55170c4ab25cd1.webp 768w, /tutorials/nextcloud-eigene-cloud/nextcloud-dateien_hu_2b1999b825fcf10.webp 1200w, /tutorials/nextcloud-eigene-cloud/nextcloud-dateien_hu_b18f6036e55a9058.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/nextcloud-eigene-cloud/nextcloud-dateien_hu_eb10dcee28315b64.webp"
    alt="Die Dateien-Oberfläche von Nextcloud mit Ordnern und Beispieldateien" title="Die Dateien-App – das Herzstück deiner eigenen Cloud"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Dateien-App – das Herzstück deiner eigenen Cloud</figcaption></figure></p>
<h3 id="schritt-5-hintergrund-jobs-cron-einrichten">Schritt 5: Hintergrund-Jobs (Cron) einrichten</h3>
<p>Nextcloud muss regelmäßig Aufgaben erledigen (Aufräumen, Benachrichtigungen,
Vorschaubilder). Standardmäßig passiert das per „AJAX&quot; bei jedem Seitenaufruf – das ist
unzuverlässig. Der empfohlene Weg ist ein <strong>Cron-Sidecar</strong>: ein zweiter Container mit
demselben Image, der nur <code>cron.php</code> ausführt. Ergänze in der <code>compose.yaml</code> einen
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="w">  </span><span class="nt">nc-cron</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">nextcloud:34-apache</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">nc-cron</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">/cron.sh</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">nc-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">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">nc_html:/var/www/html</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">default]</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></span></code></pre></div>
</div>
<p>Der Sidecar teilt sich das <code>nc_html</code>-Volume mit der App und führt die Jobs alle fünf
Minuten aus. Übernimm die Änderung und stell den Modus in Nextcloud auf „Cron&quot;:</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 <span class="nb">exec</span> -u www-data nc-app php occ background:cron</span></span></code></pre></div>
</div>
<p>Unter <strong>Verwaltung → Grundeinstellungen</strong> sollte jetzt „Cron&quot; aktiv sein.</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>occ – das Kommandozeilen-Werkzeug
  </p>
  <div class="prose-kitchen text-sm"><p><code>occ</code> ist Nextclouds Admin-Werkzeug. Du rufst es immer als Nutzer <code>www-data</code> im
App-Container auf: <code>docker exec -u www-data nc-app php occ &lt;befehl&gt;</code>. Zwei nützliche
Aufräum-Befehle direkt nach der Installation:</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 <span class="nb">exec</span> -u www-data nc-app php occ db:add-missing-indices
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:set default_phone_region --value<span class="o">=</span>DE</span></span></code></pre></div>
</div></div>
</div>
<h3 id="schritt-6-die-clients-verbinden">Schritt 6: Die Clients verbinden</h3>
<p>Jetzt der eigentliche Nutzen. Es gibt drei Wege, deine Cloud zu benutzen:</p>
<ol>
<li><strong>Desktop-Sync-Client</strong> (Windows/macOS/Linux): Installiere den „Nextcloud Desktop&quot;-
Client, gib als Server-Adresse <code>https://cloud.DEINE_DOMAIN</code> ein, melde dich an und
wähle einen lokalen Ordner. Dateien werden wie bei Dropbox synchronisiert.
Mit den <strong>virtuellen Dateien</strong> (Windows/macOS) belegen sie erst dann Platz auf der
Platte, wenn du sie öffnest – praktisch, wenn deine Cloud größer ist als die lokale
Festplatte.</li>
<li><strong>Handy-App</strong> (Android/iOS): die offizielle Nextcloud-App, gleiche Server-Adresse.
Ideal für den automatischen <strong>Foto-Upload</strong> vom Smartphone.</li>
<li><strong>Kalender &amp; Kontakte:</strong> In den Systemeinstellungen deines Geräts ein CalDAV-/
CardDAV-Konto mit der Adresse <code>https://cloud.DEINE_DOMAIN</code> anlegen – dank der
<code>.well-known</code>-Middleware aus Schritt 3 funktioniert die automatische Erkennung.</li>
</ol>
<p>Funktionen wie Kalender, Kontakte oder Mail sind eigene <strong>Apps</strong>, die du unter
<strong>Verwaltung → Apps</strong> mit einem Klick nachinstallierst – Nextcloud ist also erweiterbar,
ohne dass du am Server etwas anfassen musst.</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>Nicht mit dem Admin-Konto arbeiten
  </p>
  <div class="prose-kitchen text-sm">Das Administrationskonto aus Schritt 4 ist für die <strong>Verwaltung</strong> gedacht, nicht für den
Alltag. Leg dir unter <strong>Verwaltung → Konten</strong> ein normales Nutzerkonto an und synchronisiere
deine Dateien darüber. So ist dein mächtiges Admin-Konto nicht ständig auf allen Geräten
angemeldet – ein einfacher, aber wirksamer Sicherheitsgewinn. Für Familie oder Team legst
du hier weitere Konten an; jedes bekommt seinen eigenen, verschlüsselten Bereich.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> „<strong>Zugriff über eine nicht vertrauenswürdige Domäne</strong>&quot; statt der Login-Seite.</p>
<p><strong>Ursache &amp; Lösung:</strong> Deine Domain steht nicht in <code>trusted_domains</code>. Prüfe
<code>NEXTCLOUD_TRUSTED_DOMAINS</code> in der Compose-Datei. Nachträglich setzen geht per occ:
<code>docker exec -u www-data nc-app php occ config:system:set trusted_domains 1 --value=cloud.DEINE_DOMAIN</code>.</p>
<p><strong>Symptom:</strong> Der Sicherheits-Check meldet „<strong>Your web server is not set up properly to
resolve .well-known/caldav</strong>&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> Der CalDAV/CardDAV-Redirect greift nicht. Kontrolliere die
<code>nc-dav</code>-Middleware-Labels (Schritt 3) und dass der Router sie über
<code>...routers.nc.middlewares=nc-dav</code> einbindet. Das Regex muss die volle
<code>https://…</code>-URL matchen – <code>redirectregex</code> prüft die komplette URL, nicht nur den Pfad.</p>
<p><strong>Symptom:</strong> Warnung „<strong>The &lsquo;Strict-Transport-Security&rsquo; HTTP header is not set</strong>&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> Der HSTS-Header fehlt. Setz ihn als Traefik-Middleware und häng sie
an den Router:
<code>traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000</code>. Häng sie zusätzlich zur
<code>nc-dav</code>-Middleware an den Router (<code>...routers.nc.middlewares=nc-dav,nc-secure</code>). Der Header
muss vom Proxy kommen, nicht von Nextcloud. In
<a href="/tutorials/nextcloud-optimieren-absichern/">Teil 2</a> richten wir genau diese
<code>nc-secure</code>-Middleware vollständig ein (inklusive <code>includeSubdomains</code>).</p>
<p><strong>Symptom:</strong> Große Uploads brechen ab oder enden mit einem Timeout / „413&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> Das PHP-Limit ist zu klein. Erhöhe <code>PHP_UPLOAD_LIMIT</code> und
<code>PHP_MEMORY_LIMIT</code> (Schritt 3) und starte den Container neu. In seltenen Fällen greifen
diese Variablen nicht – dann ein eigenes <code>php.ini</code>-Snippet ins Image mounten.</p>
<p><strong>Symptom:</strong> Beim ersten Start bricht die Installation mit „<strong>MySQL server has gone
away</strong>&quot; oder „Connection refused&quot; ab.</p>
<p><strong>Ursache &amp; Lösung:</strong> Der App-Container war schneller als die Datenbank. Genau dagegen
ist der <code>healthcheck</code> mit <code>depends_on: condition: service_healthy</code> da – prüfe, dass beide
in deiner Compose-Datei vorhanden sind, und starte mit <code>docker compose up -d</code> neu.</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li>
<p><strong>Backups sind bei einer Cloud Pflicht.</strong> Ein konsistentes Backup braucht <strong>drei
Dinge</strong>: die <strong>Datenbank</strong> (MariaDB-Dump), das <strong>Datenverzeichnis</strong> (deine Dateien) und
die <strong>Konfiguration</strong> (<code>config.php</code>). Versetze Nextcloud vor dem Backup kurz in den
Wartungsmodus, damit Datenbank und Dateien zueinander passen:</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 <span class="nb">exec</span> -u www-data nc-app php occ maintenance:mode --on
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> nc-db mariadb-dump -u root -pEIN_STARKES_ROOT_PASSWORT nextcloud &gt; nextcloud-db.sql
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ maintenance:mode --off</span></span></code></pre></div>
</div>
<p>Sichere den Dump zusammen mit dem <code>nc_html</code>-Volume <strong>verschlüsselt und off-site</strong> mit
<a href="/tutorials/backups-mit-restic/">Restic</a> – das rohe <code>nc_db</code>-Volume musst du dank des
Dumps nicht zusätzlich sichern. Im <code>nc_html</code>-Volume stecken sowohl der
Programmcode und die <code>config.php</code> als auch – unter <code>data/</code> – <strong>deine eigentlichen
Nutzerdateien</strong>. Wächst deine Cloud stark, kannst du dieses <code>data/</code>-Verzeichnis später
auf ein eigenes, größeres Volume auslagern; fürs erste reicht ein Volume für alles.</p>
</li>
<li>
<p><strong>Updates: immer nur eine Hauptversion pro Schritt.</strong> Von 34 auf 35, nie direkt auf 36.
Setz den Image-Tag hoch (<code>nextcloud:35-apache</code>), dann <code>docker compose pull</code> und
<code>docker compose up -d</code>. Der Container führt das nötige <code>occ upgrade</code> beim Start
<strong>automatisch</strong> aus. <strong>Vorher immer ein Backup</strong> anlegen. Den Cron-Sidecar auf dieselbe
Version heben.</p>
</li>
<li>
<p><strong>Sicherheit prüfen.</strong> Lass nach dem Aufsetzen den offiziellen
<a href="https://scan.nextcloud.com/">Nextcloud Security Scan</a> gegen deine Domain laufen (Ziel:
Note A) und arbeite die Warnungen aus der Admin-Übersicht ab. Die Übersicht unter
<strong>Verwaltung → Übersicht</strong> führt automatische Prüfungen durch und zeigt genau, was noch
fehlt. Halte Nextcloud aktuell – Sicherheitslücken werden zeitnah geschlossen, aber nur,
wenn du updatest.</p>
</li>
<li>
<p><strong>Ehrlich zum Aufwand:</strong> Eine selbst gehostete Cloud will gepflegt werden. Rechne mit
einem Update-Durchgang etwa monatlich und einem regelmäßigen Blick in die
Admin-Übersicht. Dafür gehören die Daten dann wirklich dir.</p>
</li>
</ul>
]]></content:encoded></item><item><title>Uptime Kuma installieren: Server-Monitoring hinter Traefik</title><link>https://serverkueche.de/tutorials/uptime-kuma-monitoring/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/uptime-kuma-monitoring/</guid><description>Uptime Kuma hinter Traefik aufsetzen und deine Dienste überwachen: Monitore, Benachrichtigungen und eine Status-Seite – die erste echte App der Serie.</description><content:encoded><![CDATA[<p>Traefik steht – jetzt hängen wir die erste echte App dahinter. <strong>Uptime Kuma</strong> ist
dafür ideal: winzig, sofort nützlich und ohne Datenbank. Es überwacht deine Dienste
und schlägt Alarm, wenn einer ausfällt – ab jetzt merkst <em>du</em> zuerst, dass etwas
klemmt, nicht deine Nutzer. Nebenbei lernst du das Muster, das jede weitere App
wiederholt.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Uptime Kuma 2.4</strong> unter <code>https://status.DEINE_DOMAIN</code>, abgesichert
über Traefik mit automatischem HTTPS. Du hast den ersten Monitor eingerichtet (der
prüft im Minutentakt, ob ein Dienst antwortet), eine Benachrichtigung verbunden und
optional eine öffentliche Status-Seite. Zum ersten Mal binden wir dabei einen Dienst
an, der <strong>nicht auf Port 80</strong> lauscht – ein wichtiges Detail für alle folgenden Apps.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein laufender <a href="/tutorials/reverse-proxy-traefik/">Traefik-Reverse-Proxy</a> samt
<code>proxy</code>-Netzwerk und funktionierendem Let&rsquo;s-Encrypt-Resolver <code>le</code></li>
<li>Ein DNS-Record <code>status.DEINE_DOMAIN</code>, der
<a href="/tutorials/domain-mit-server-verbinden/">auf den Server zeigt</a></li>
</ul>
<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-die-composeyaml">Schritt 1: Die compose.yaml</h3>
<p>Eigener Ordner, eigene Datei:</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 ~/uptime-kuma <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/uptime-kuma</span></span></code></pre></div>
</div>
<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="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">uptime-kuma</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">louislam/uptime-kuma:2</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">kuma-data:/app/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></span><span class="line"><span class="cl"><span class="w">      </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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.kuma.rule=Host(`status.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.kuma.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.kuma.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.kuma.loadbalancer.server.port=3001&#34;</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></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">kuma-data</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">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>Das kennst du aus dem Traefik-Tutorial – bis auf <strong>eine neue, entscheidende Zeile</strong>:</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="s2">&#34;traefik.http.services.kuma.loadbalancer.server.port=3001&#34;</span></span></span></code></pre></div>
</div>
<p>Uptime Kuma lauscht intern auf <strong>Port 3001</strong>, nicht auf 80. Diese Zeile sagt Traefik,
an welchen Port es die Anfragen weiterreichen soll. Ohne sie rät Traefik falsch und
du bekommst einen <code>Bad Gateway</code>. Merke dir das Label – jede App, die nicht auf Port
80 läuft, braucht es.</p>
<p>Der Rest ist das bekannte Muster: <strong>kein <code>ports:</code></strong> (nur über Traefik erreichbar),
<code>proxy</code>-Netzwerk, Named Volume für die Daten.</p>
<h3 id="schritt-2-starten-und-ersten-login">Schritt 2: Starten und ersten Login</h3>
<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 logs -f uptime-kuma</span></span></code></pre></div>
</div>
<p>Wenn Kuma bereit ist, endet das Log mit dieser Zeile – ab hier nimmt der Dienst
Anfragen an (<code>Strg+C</code> beendet nur das Mitlesen):</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">[SERVER] INFO: Welcome to Uptime Kuma
</span></span><span class="line"><span class="cl">[SERVER] INFO: Uptime Kuma Version: 2.4.0
</span></span><span class="line"><span class="cl">[SETUP-DATABASE] INFO: Listening on:
</span></span><span class="line"><span class="cl">[SETUP-DATABASE] INFO: -  http://localhost:3001</span></span></code></pre></div>
</div>
<p>Beim ersten Start legt Kuma seine Datenbank im Volume an – das dauert einige
Sekunden. Ruf dann <code>https://status.DEINE_DOMAIN</code> im Browser auf. Uptime Kuma 2.x
fragt zuerst die <strong>Datenbank</strong> ab – für ein Setup wie unseres ist <strong>SQLite</strong> die
richtige, einfachste Wahl (auswählen, auf <strong>Weiter</strong> klicken). Direkt danach legst du
das <strong>Admin-Konto</strong> an (Benutzername + starkes Passwort).</p>
<p><figure class="my-6"><img src="/tutorials/uptime-kuma-monitoring/kuma-setup_hu_efd590bbed8353ae.webp" srcset="/tutorials/uptime-kuma-monitoring/kuma-setup_hu_d81de5823c4df6b.webp 480w, /tutorials/uptime-kuma-monitoring/kuma-setup_hu_efd590bbed8353ae.webp 768w, /tutorials/uptime-kuma-monitoring/kuma-setup_hu_a19456213e4e011c.webp 1200w, /tutorials/uptime-kuma-monitoring/kuma-setup_hu_5b8bc0792bdf01af.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/uptime-kuma-monitoring/kuma-setup_hu_7c7eee8b248ca320.webp"
    alt="Uptime Kumas Ersteinrichtung: Sprache wählen und das Admin-Konto anlegen" title="Ersteinrichtung: Sprache wählen und Admin-Konto anlegen"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Ersteinrichtung: Sprache wählen und Admin-Konto anlegen</figcaption></figure></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>Sofort das Admin-Konto anlegen
  </p>
  <div class="prose-kitchen text-sm">Solange kein Admin-Konto existiert, kann <strong>jeder</strong>, der die Seite aufruft, eines
anlegen. Erledige das direkt nach dem ersten Start – nicht „später&quot;.</div>
</div>
<h3 id="schritt-3-den-ersten-monitor-anlegen">Schritt 3: Den ersten Monitor anlegen</h3>
<p>Ein <strong>Monitor</strong> ist eine wiederkehrende Prüfung. Klicke auf <strong>Neuen Monitor
hinzufügen</strong> und lege einen für Traefik selbst an:</p>
<ul>
<li><strong>Monitortyp:</strong> <code>HTTP(s)</code></li>
<li><strong>Freundlicher Name:</strong> <code>Traefik Dashboard</code></li>
<li><strong>URL:</strong> <code>https://traefik.DEINE_DOMAIN</code></li>
<li><strong>Prüfintervall:</strong> <code>60</code> Sekunden</li>
<li><strong>Authentifizierung:</strong> Das Traefik-Dashboard ist per Basic-Auth geschützt
(Schritt 7 des <a href="/tutorials/reverse-proxy-traefik/">Traefik-Tutorials</a>) – wähle
deshalb unten <strong>HTTP Basic Auth</strong> als Authentifizierungsmethode und trage
Benutzer und Passwort ein. Ohne Zugangsdaten bekommt Kuma nur <code>401</code> und meldet
den Monitor als <strong>Down</strong>.</li>
</ul>
<p>Speichern – nach wenigen Sekunden steht der Monitor auf <strong>Online</strong> (grün) und zeigt
die Antwortzeit.</p>
<p><figure class="my-6"><img src="/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_784a44c058559b8d.webp" srcset="/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_1971a2e8e4bcb854.webp 480w, /tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_784a44c058559b8d.webp 768w, /tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_5e8aad21b68e4d26.webp 1200w, /tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_bc95a5405a757e2b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_2d577b17b1d1a329.webp"
    alt="Das Uptime-Kuma-Dashboard mit zwei laufenden Monitoren – beide grün mit 100 % Verfügbarkeit und „200 – OK&quot;" title="Das Dashboard mit zwei laufenden Monitoren"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Dashboard mit zwei laufenden Monitoren</figcaption></figure></p>
<p>Neben dem einfachen <code>HTTP(s)</code>-Check lohnt es sich, den passenden <strong>Monitortyp</strong> zu
wählen:</p>
<ul>
<li><strong>HTTP(s) – Keyword:</strong> prüft zusätzlich, ob ein bestimmtes Wort im Antworttext
steht. So erkennst du „Server antwortet zwar, zeigt aber eine Fehlerseite&quot;.</li>
<li><strong>TCP-Port:</strong> für Dienste ohne Web-Oberfläche (z. B. eine Datenbank, ein
SSH-Port). Prüft nur, ob der Port offen ist.</li>
<li><strong>Ping:</strong> einfachster Erreichbarkeitstest per ICMP.</li>
<li><strong>Docker-Container:</strong> prüft direkt den Container-Status über den Docker-Socket –
praktisch für interne Dienste ohne eigene Domain.</li>
<li><strong>Push:</strong> hier ruft <em>der überwachte Dienst</em> Kuma regelmäßig an. Ideal für Cronjobs
und Backups: meldet sich das Skript nicht rechtzeitig, schlägt Kuma Alarm
(„Dead man&rsquo;s switch&quot;).</li>
</ul>
<p>Lege für jeden wichtigen Dienst den passenden Monitor an.</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>Tipp
  </p>
  <div class="prose-kitchen text-sm">Prüfe <strong>öffentliche Dienste über ihre echte Domain</strong> (<code>https://…</code>), nicht über
<code>localhost</code>. So testest du zugleich, dass Traefik und das Zertifikat von außen
funktionieren – nicht nur, dass der Container läuft.</div>
</div>
<p>Beim <code>HTTP(s)</code>-Monitor lohnt sich zusätzlich die Option
<strong>„Zertifikatsablauf-Benachrichtigung&quot;</strong>: Kuma warnt dann rechtzeitig, bevor ein
TLS-Zertifikat ausläuft. Bei Diensten hinter Traefik erneuert Let&rsquo;s Encrypt zwar
automatisch – aber genau dieser Automatismus fällt gelegentlich leise aus (ein
DNS-Record wird geändert, Port 80 versehentlich zugemacht). Der Monitor ist dein
Sicherheitsnetz und meldet sich, solange noch Tage zum Reparieren bleiben, statt dass
Besucher plötzlich vor einer Zertifikatswarnung stehen.</p>
<h3 id="schritt-4-benachrichtigungen-einrichten-e-mail-und-telegram">Schritt 4: Benachrichtigungen einrichten (E-Mail und Telegram)</h3>
<p>Ein Monitor ohne Alarm ist nur ein hübsches Diagramm. Unter <strong>Einstellungen →
Benachrichtigungen → Benachrichtigung einrichten</strong> legst du einen Kanal an. Uptime
Kuma unterstützt über 90 – wir richten die zwei häufigsten komplett ein: <strong>E-Mail</strong>
für die klassische Nachricht und <strong>Telegram</strong> für Push direkt aufs Handy.</p>
<h4 id="e-mail-smtp">E-Mail (SMTP)</h4>
<p>Wähle als <strong>Benachrichtigungsdienst</strong> „E-Mail (SMTP)&quot; und trage die Daten deines
Mailanbieters ein:</p>
<ul>
<li><strong>Hostname / Port:</strong> z. B. <code>smtp.DEIN_ANBIETER.de</code> und <code>587</code></li>
<li><strong>Sicherheit:</strong> <code>STARTTLS</code> (Port 587) oder <code>TLS/SSL</code> (Port 465)</li>
<li><strong>Benutzername / Passwort:</strong> deine SMTP-Zugangsdaten</li>
<li><strong>Absender- / Empfänger-Adresse:</strong> von welcher Adresse die Warnung kommt und an
welche sie geschickt wird</li>
</ul>
<p><figure class="my-6"><img src="/tutorials/uptime-kuma-monitoring/kuma-email_hu_d2820d36f82b537d.webp" srcset="/tutorials/uptime-kuma-monitoring/kuma-email_hu_d4dc428dbefa278e.webp 480w, /tutorials/uptime-kuma-monitoring/kuma-email_hu_d2820d36f82b537d.webp 768w, /tutorials/uptime-kuma-monitoring/kuma-email_hu_6c4b750c2a8c2946.webp 1200w, /tutorials/uptime-kuma-monitoring/kuma-email_hu_a8c700607ce6ed62.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/uptime-kuma-monitoring/kuma-email_hu_c3b1043311d6491b.webp"
    alt="Die E-Mail-Benachrichtigung (SMTP) in Uptime Kuma: Hostname, Port, Sicherheit und Absenderadresse" title="E-Mail-Benachrichtigung (SMTP) einrichten"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">E-Mail-Benachrichtigung (SMTP) einrichten</figcaption></figure></p>
<p>Klick auf <strong>Test</strong> – innerhalb weniger Sekunden landet eine Testnachricht im
Postfach. Erst wenn die wirklich ankommt, stimmen Zugangsdaten und Port. Speichern
nicht vergessen.</p>
<h4 id="telegram">Telegram</h4>
<p>Telegram ist ideal für sofortige Push-Alarme aufs Handy – ohne eigenen Mailserver.
Du brauchst zwei Angaben:</p>
<ol>
<li><strong>Bot-Token:</strong> Schreib in Telegram <a href="https://t.me/BotFather">@BotFather</a> an, sende
<code>/newbot</code>, vergib einen Namen – BotFather antwortet mit dem <strong>Token</strong>.</li>
<li><strong>Chat-ID:</strong> Schreib deinem neuen Bot eine beliebige Nachricht, ruf dann
<code>https://api.telegram.org/bot&lt;DEIN_TOKEN&gt;/getUpdates</code> im Browser auf und lies die
<code>chat.id</code> aus der Antwort.</li>
</ol>
<p>Trage Token und Chat-ID in die Telegram-Benachrichtigung ein (Uptime Kuma verlinkt
beide Hilfen direkt im Dialog) und klick <strong>Test</strong> – die Nachricht sollte sofort im
Chat erscheinen.</p>
<p><figure class="my-6"><img src="/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_47456c4bec88e7e5.webp" srcset="/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_fb559c371c833a22.webp 480w, /tutorials/uptime-kuma-monitoring/kuma-telegram_hu_47456c4bec88e7e5.webp 768w, /tutorials/uptime-kuma-monitoring/kuma-telegram_hu_ff4d40c3b064e2f8.webp 1200w, /tutorials/uptime-kuma-monitoring/kuma-telegram_hu_211e84e50a4d3d92.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_ba0db8d5a803ec3.webp"
    alt="Die Telegram-Benachrichtigung in Uptime Kuma: Bot-Token und Chat-ID, mit Direktlink zum BotFather" title="Telegram-Benachrichtigung: Bot-Token und Chat-ID"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Telegram-Benachrichtigung: Bot-Token und Chat-ID</figcaption></figure></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>Sonst bleibt der Alarm stumm
  </p>
  <div class="prose-kitchen text-sm">Eine eingerichtete Benachrichtigung greift <strong>nicht automatisch</strong>. Aktiviere sie in
jedem Monitor (Häkchen im Monitor-Formular) oder schalte im Benachrichtigungs-Dialog
„Standardmäßig aktiviert&quot; ein und „Auf alle existierenden Monitore anwenden&quot;.</div>
</div>
<p>Willst du Push-Nachrichten komplett in Eigenregie (ohne Telegram-Server), kommt
später <strong>ntfy</strong> dazu – selbst gehostet, mit eigenem Rezept in der Serie.</p>
<h3 id="schritt-5-eine-öffentliche-status-seite-optional">Schritt 5: Eine öffentliche Status-Seite (optional)</h3>
<p>Kuma kann eine <strong>Status-Seite</strong> veröffentlichen, auf der Besucher sehen, ob deine
Dienste laufen. So gehst du vor:</p>
<ol>
<li><strong>Statusseiten → Neue Status-Seite</strong>, dann <strong>Name</strong> (z. B. „Serverküche Status&quot;)
und <strong>Slug</strong> (die URL, z. B. <code>serverkueche</code>) vergeben und auf <strong>Weiter</strong> klicken.</li>
<li>Im Editor <strong>Gruppe hinzufügen</strong> (z. B. „Dienste&quot;), darunter über das Auswahlfeld
die gewünschten <strong>Monitore</strong> hinzufügen.</li>
<li>Oben rechts <strong>Speichern</strong> – fertig. Öffentlich erreichbar ist die Seite dann unter
<code>https://status.DEINE_DOMAIN/status/serverkueche</code>.</li>
</ol>
<p>Nimm nur auf, was wirklich jeder sehen darf – interne Dienste besser weglassen.</p>
<p><figure class="my-6"><img src="/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_8dcf3c3b4a248dc0.webp" srcset="/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_919f065502781fb5.webp 480w, /tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_8dcf3c3b4a248dc0.webp 768w, /tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_77bf40b6f6dc2672.webp 1200w, /tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_10c7a7de5007888a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_af2b2c49fa2d50d3.webp"
    alt="Die öffentliche Status-Seite „Serverküche Status&quot;: „Alle Systeme funktionsfähig&quot; und beide Dienste mit 100 % Verfügbarkeit" title="Die fertige öffentliche Status-Seite"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die fertige öffentliche Status-Seite</figcaption></figure></p>
<h3 id="schritt-6-fehlalarme-vermeiden--das-alarm-verhalten-feinjustieren">Schritt 6: Fehlalarme vermeiden – das Alarm-Verhalten feinjustieren</h3>
<p>Ein Monitor, der bei jedem kurzen Netzwerk-Schluckauf Alarm schlägt, wird schnell
ignoriert – und dann verpasst du den echten Ausfall. Im Monitor-Formular stellst du
das Verhalten passend ein:</p>
<ul>
<li><strong>Wiederholungen (Retries):</strong> Erst nach <em>n</em> fehlgeschlagenen Prüfungen gilt der
Dienst als „Down&quot;. <code>2</code>–<code>3</code> filtert einzelne Aussetzer heraus, ohne echte Ausfälle
lange zu verschleiern.</li>
<li><strong>Heartbeat-Intervall bei Ausfall:</strong> Kuma darf im Fehlerfall häufiger prüfen
(z. B. alle 20 Sekunden), um die Erholung schnell zu erkennen.</li>
<li><strong>Erneut benachrichtigen:</strong> Kuma kann dich alle <em>x</em> Minuten erinnern, solange ein
Dienst down ist – nützlich, damit ein Ausfall nachts nicht in einer einzigen
Mail untergeht.</li>
</ul>
<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>Wartungsfenster einplanen
  </p>
  <div class="prose-kitchen text-sm">Planst du ein Update mit Downtime, lege unter <strong>Wartung</strong> ein <strong>Wartungsfenster</strong> an.
Kuma pausiert dann die Alarme für die betroffenen Monitore – so bekommst du (und die
Status-Seite) keine Fehlalarme, während du selbst am Werk bist.</div>
</div>
<h3 id="schritt-7-cronjobs-überwachen-mit-einem-push-monitor">Schritt 7: Cronjobs überwachen mit einem Push-Monitor</h3>
<p>Klassische Monitore prüfen von außen, ob ein Dienst <em>antwortet</em>. Für Dinge, die
<strong>still im Hintergrund laufen</strong> – ein nächtliches Backup, ein Sync-Skript, ein
Cronjob – dreht der <strong>Push-Monitor</strong> das Prinzip um: Nicht Kuma fragt an, sondern
<em>dein Skript meldet sich</em>. Bleibt die Meldung aus, schlägt Kuma Alarm – der klassische
„Dead man&rsquo;s switch&quot;.</p>
<p>Lege einen Monitor vom Typ <strong>Push</strong> an. Kuma zeigt dir dann eine eindeutige
<strong>Push-URL</strong>:</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">https://status.DEINE_DOMAIN/api/push/DEIN_TOKEN?status=up&amp;msg=OK&amp;ping=</span></span></code></pre></div>
</div>
<p>Diese URL rufst du am Ende deines Skripts auf – zum Beispiel nach einem erfolgreich
durchgelaufenen Backup:</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="c1"># ... dein Backup-Befehl ...</span>
</span></span><span class="line"><span class="cl">curl -fsS <span class="s2">&#34;https://status.DEINE_DOMAIN/api/push/DEIN_TOKEN?status=up&amp;msg=Backup+OK&#34;</span> &gt; /dev/null</span></span></code></pre></div>
</div>
<p>Stell das <strong>Prüfintervall</strong> in Kuma etwas großzügiger ein als deinen Cron-Takt (läuft
das Backup stündlich, gib Kuma z. B. 90 Minuten Toleranz). Kommt in dieser Zeit kein
<code>curl</code>, geht der Monitor auf <strong>Down</strong> und du wirst benachrichtigt – du erfährst also
von einem <em>nicht</em> gelaufenen Backup, nicht erst, wenn du es dringend brauchst.</p>
<h3 id="schritt-8-den-admin-login-mit-2fa-absichern">Schritt 8: Den Admin-Login mit 2FA absichern</h3>
<p>Dein Kuma-Login schützt den Zugang zu allen Monitoren, den hinterlegten
Benachrichtigungs-Zugangsdaten und der Status-Seiten-Konfiguration – und er hängt
öffentlich im Netz. Schalte deshalb <strong>Zwei-Faktor-Authentifizierung</strong> ein: unter
<strong>Einstellungen → Sicherheit → Zwei-Faktor-Authentifizierung</strong>. Kuma zeigt einen
QR-Code, den du mit einer Authenticator-App (z. B. Aegis oder 2FAS) scannst; zum
Aktivieren gibst du einmal den erzeugten Code ein.</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>Wiederherstellung absichern
  </p>
  <div class="prose-kitchen text-sm">Bewahre das TOTP-Secret bzw. einen zweiten Authenticator an einem sicheren Ort auf
(Passwortmanager). Verlierst du dein Telefon <strong>und</strong> hast keine Kopie, kommst du sonst
nur noch über die Datenbank im <code>kuma-data</code>-Volume wieder hinein.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> <code>Bad Gateway</code> (502) beim Aufruf von <code>status.DEINE_DOMAIN</code>.</p>
<p><strong>Ursache &amp; Lösung:</strong> Fast immer fehlt das Port-Label
<code>traefik.http.services.kuma.loadbalancer.server.port=3001</code> oder es steht ein
falscher Port drin. Traefik erreicht den Container dann zwar, klopft aber am
falschen Port an. Label prüfen und <code>docker compose up -d</code> erneut ausführen.</p>
<p><strong>Symptom:</strong> <code>404 page not found</code> statt Kuma.</p>
<p><strong>Ursache &amp; Lösung:</strong> Wie bei jeder App hinter Traefik: <code>traefik.enable=true</code>
gesetzt? Container im <code>proxy</code>-Netzwerk? Stimmt die Domain in der <code>Host(...)</code>-Regel
und zeigt der DNS-Record <code>status.DEINE_DOMAIN</code> auf den Server? Das Traefik-Dashboard
zeigt unter „HTTP Routers&quot;, ob <code>kuma</code> registriert ist.</p>
<p><strong>Symptom:</strong> Die Oberfläche lädt, aber die Live-Aktualisierung ruckelt / bricht ab.</p>
<p><strong>Ursache &amp; Lösung:</strong> Kuma nutzt WebSockets. Traefik leitet die standardmäßig korrekt
weiter – tritt das Problem trotzdem auf, liegt es meist an einem davorgeschalteten
CDN/Proxy (z. B. Cloudflare im „Proxy&quot;-Modus), der WebSockets blockt. Für den
Direktbetrieb hinter Traefik ist keine Zusatzkonfiguration nötig.</p>
<p><strong>Symptom:</strong> Nach einem Neuaufsetzen sind alle Monitore weg.</p>
<p><strong>Ursache &amp; Lösung:</strong> Das <code>kuma-data</code>-Volume wurde gelöscht (z. B. durch
<code>docker compose down -v</code>). Alle Konfiguration und Historie liegt allein in diesem
Volume – deshalb steht es im nächsten Abschnitt ganz oben.</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li><strong>Sichern:</strong> Das komplette Herz von Kuma ist das Volume <code>kuma-data</code> (eine
SQLite-Datenbank). Sichere es regelmäßig – ist es weg, sind alle Monitore und die
Historie weg. Das Off-Site-Backup dafür bauen wir im
<a href="/tutorials/backups-mit-restic/">Restic-Tutorial</a>.</li>
<li><strong>Updates:</strong> Tag <code>:2</code> bleibt bei der 2.x-Reihe und bringt Fehlerbehebungen mit
<code>docker compose pull &amp;&amp; docker compose up -d</code>. Vor einem Sprung auf eine neue
Hauptversion (z. B. später <code>:3</code>) die Release-Notes lesen und vorher das Volume
sichern.</li>
<li><strong>Von 1.x kommend?</strong> Der Wechsel auf <code>:2</code> <strong>migriert die SQLite-Datenbank beim
ersten Start automatisch</strong> – das kann einen Moment dauern, und ein Zurück auf <code>:1</code>
ist danach nicht vorgesehen. Sichere deshalb <strong>vorher</strong> das <code>kuma-data</code>-Volume, dann
bist du auf der sicheren Seite. Neu-Installationen (wie oben) betrifft das nicht.</li>
<li><strong>Ehrliche Einschränkung:</strong> Ein Monitor, der <strong>auf demselben Server</strong> läuft wie die
überwachten Dienste, kann dich nicht warnen, wenn der ganze Server ausfällt – dann
ist auch Kuma offline. Ergänze für den Ernstfall einen <strong>externen</strong> Wächter. Zwei
günstige Wege: ein <strong>zweites Uptime Kuma</strong> auf einem kleinen Server (oder zu Hause),
das nur diese Instanz per HTTP überwacht – oder ein <strong>kostenloser externer
Ping-Dienst</strong>, der deine öffentliche Status-Seite anpingt. So bekommst du auch dann
eine Meldung, wenn der ganze Host weg ist – der einzige Fall, den ein lokaler
Monitor prinzipbedingt nicht abdecken kann.</li>
</ul>
<p>Damit hast du das App-Muster verinnerlicht und überwachst ab sofort alles, was du
hinter Traefik hängst. Was jede weitere App voraussetzt, sind
<a href="/tutorials/backups-mit-restic/">verschlüsselte Off-Site-Backups mit Restic</a> –
damit deine Daten einen Servercrash überleben.</p>
]]></content:encoded></item><item><title>Vaultwarden: eigener Passwortmanager hinter Traefik</title><link>https://serverkueche.de/tutorials/vaultwarden-passwortmanager/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/vaultwarden-passwortmanager/</guid><description>Vaultwarden selbst hosten: ein schlanker, Bitwarden-kompatibler Passwortmanager hinter Traefik mit HTTPS, Admin-Panel und verschlüsseltem Backup.</description><content:encoded><![CDATA[<p>Passwörter gehören in einen Passwortmanager – aber müssen sie in der Cloud eines
fremden Anbieters liegen? Mit <strong>Vaultwarden</strong> hostest du deinen eigenen, verschlüsselt
und unter deiner Kontrolle. Er spricht die Bitwarden-Sprache, also nutzt du die
gewohnten Bitwarden-Apps und -Browser-Erweiterungen – nur zeigen sie auf <strong>deinen</strong>
Server.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Vaultwarden 1.36</strong> in einem Container hinter deinem Traefik-Proxy,
erreichbar unter <code>https://vault.DEINE_DOMAIN</code> mit gültigem HTTPS-Zertifikat. Vaultwarden
ist eine schlanke, in Rust geschriebene Neuimplementierung des Bitwarden-Servers:
API-kompatibel, aber sparsam genug, um auf einem kleinen VPS zu laufen. Du verbindest
die offiziellen <strong>Bitwarden-Clients</strong> (Browser-Erweiterung, Handy-App, Desktop) mit
deinem Server, legst deine Zugangsdaten ab – Ende-zu-Ende verschlüsselt – und
verwaltest den Dienst über ein abgesichertes <strong>Admin-Panel</strong>.</p>
<p>Deine Passwörter liegen damit auf deinem Server, in deinem Land, unter deinem Backup –
und du zahlst keine Abo-Gebühr für Funktionen wie 2FA oder Attachments.</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>Vaultwarden ≠ Bitwarden
  </p>
  <div class="prose-kitchen text-sm">Vaultwarden ist ein <strong>inoffizielles</strong>, community-getriebenes Projekt, das die
Bitwarden-Server-API nachbaut. Es ist nicht mit Bitwarden Inc. verbunden. Für den
privaten und kleinen Team-Einsatz ist es hervorragend geeignet; die Clients (Apps,
Erweiterungen) sind die echten, offiziellen von Bitwarden.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein laufender <strong>Traefik-Reverse-Proxy</strong> mit dem gemeinsamen <code>proxy</code>-Netzwerk und dem
Let&rsquo;s-Encrypt-Resolver <code>le</code> – genau das Setup aus dem Tutorial
<a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a>. Ohne Traefik
funktioniert dieses Rezept nicht: Vaultwarden <strong>verlangt HTTPS</strong>.</li>
<li>Eine Subdomain, z. B. <code>vault.DEINE_DOMAIN</code>, deren DNS-Record (A/AAAA) auf deine
Server-IP zeigt – siehe <a href="/tutorials/domain-mit-server-verbinden/">Domain mit Server verbinden</a>.</li>
<li>Ein funktionierendes <strong>Backup</strong> deines Servers. Ein Passwortmanager ist der Ort, an
dem Datenverlust am meisten wehtut – richte, falls noch nicht geschehen, zuerst
<a href="/tutorials/backups-mit-restic/">Backups mit Restic</a> ein.</li>
</ul>
<p>Willst du später mehrere Dienste kombinieren, hilft der <a href="/serverempfehlung/">Server-Rechner</a>
bei der passenden Server-Größe.</p>
<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">
  <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 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 10,36 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Vaultwarden ist so sparsam, dass schon der kleinste VPS locker reicht.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12-12m?ref=44083" rel="sponsored noopener" target="_blank"
   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"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 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">
  <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-den-admin_token-erzeugen">Schritt 1: Den ADMIN_TOKEN erzeugen</h3>
<p>Vaultwarden hat ein Admin-Panel unter <code>/admin</code>. Zugriff darauf schützt ein
<code>ADMIN_TOKEN</code>. Den solltest du <strong>nicht</strong> im Klartext hinterlegen, sondern als Hash –
dann steht dein Panel-Passwort nirgends lesbar in der Compose-Datei.</p>
<p>Vaultwarden bringt dafür einen eigenen Befehl mit. Wir lassen ihn kurz in einem
Wegwerf-Container laufen:</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 run --rm -it vaultwarden/server:1.36.0 /vaultwarden <span class="nb">hash</span> --preset owasp</span></span></code></pre></div>
</div>
<p>Der Befehl fragt dich zweimal nach einem Passwort (Eingabe bleibt unsichtbar) und gibt
dann einen <strong>Argon2id-Hash</strong> aus – eine lange Zeichenkette, die so beginnt:</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">Generate an Argon2id PHC string using the &#39;owasp&#39; preset.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Password:
</span></span><span class="line"><span class="cl">Confirm Password:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ADMIN_TOKEN=&#39;$argon2id$v=19$m=19456,t=2,p=1$FkxFEQ64Wy4zlQOWMI1fJ...$TxULe6MSND3By6GPVPKB1...&#39;</span></span></code></pre></div>
</div>
<p>Kopiere die komplette Zeichenkette zwischen den Anführungszeichen (inklusive der
<code>$argon2id$…</code>-Teile) heraus – die brauchst du gleich. Das Passwort, das du hier
eingegeben hast, ist dein <strong>Admin-Panel-Passwort</strong>; leg es im Passwortmanager ab (fürs
Erste noch im alten).</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>Warum der Umweg über den Hash?
  </p>
  <div class="prose-kitchen text-sm">Du könntest den <code>ADMIN_TOKEN</code> auch als Klartext-Passwort setzen. Der Hash ist aber
sicherer: Selbst wer deine <code>compose.yaml</code> in die Finger bekommt, kann daraus dein
Panel-Passwort nicht zurückrechnen. <code>--preset owasp</code> wählt Argon2-Parameter nach der
aktuellen OWASP-Empfehlung.</div>
</div>
<h3 id="schritt-2-die-composeyaml-anlegen">Schritt 2: Die compose.yaml anlegen</h3>
<p>Leg einen eigenen Ordner an und wechsle hinein:</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 ~/vaultwarden <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/vaultwarden</span></span></code></pre></div>
</div>
<p>Erstelle die <code>compose.yaml</code>. Ersetze <code>vault.DEINE_DOMAIN</code> durch deine echte Subdomain
und den <code>ADMIN_TOKEN</code> durch den Hash aus Schritt 1:</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="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">vaultwarden</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">vaultwarden/server:1.36.0</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">./vw-data:/data</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">DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://vault.DEINE_DOMAIN&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SIGNUPS_ALLOWED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ADMIN_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;$$argon2id$$v=19$$m=19456,t=2,p=1$$FkxFE...$$TxULe...&#34;</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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.vaultwarden.rule=Host(`vault.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.vaultwarden.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.vaultwarden.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.vaultwarden.loadbalancer.server.port=80&#34;</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></span><span class="line"><span class="cl"><span class="w">      </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">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></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">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 wichtigsten Zeilen im Detail:</p>
<ul>
<li><strong><code>image: vaultwarden/server:1.36.0</code></strong> – wir pinnen die Version bewusst fest, statt
<code>latest</code> zu nehmen. So aktualisierst du kontrolliert (siehe „Wartung&quot;). Für ein noch
kleineres Image gibt es auch <code>:1.36.0-alpine</code>.</li>
<li><strong><code>volumes: ./vw-data:/data</code></strong> – hier liegen deine Datenbank (<code>db.sqlite3</code>), die
Verschlüsselungs-Keys und die Attachments. <strong>Dieses Volume ist Pflicht.</strong> Ohne
Volume verweigert Vaultwarden absichtlich den Start, damit du deine Daten nicht in
einem flüchtigen Container verlierst.</li>
<li><strong><code>DOMAIN</code></strong> – die vollständige HTTPS-URL. Vaultwarden braucht sie u. a. für WebAuthn/
2FA und Einladungslinks. Muss exakt zu deiner Router-Regel passen.</li>
<li><strong><code>SIGNUPS_ALLOWED: &quot;true&quot;</code></strong> – erlaubt vorerst die Registrierung, damit du dein
erstes Konto anlegen kannst. Das drehen wir gleich wieder ab.</li>
<li><strong><code>ADMIN_TOKEN</code></strong> – dein Hash aus Schritt 1. <strong>Wichtig:</strong> In einer Compose-Datei muss
jedes Dollarzeichen <strong>verdoppelt</strong> werden (<code>$</code> → <code>$$</code>), sonst versucht Compose, es als
Variable zu interpretieren. Aus <code>$argon2id$…</code> wird also <code>$$argon2id$$…</code>.</li>
<li><strong><code>loadbalancer.server.port=80</code></strong> – Vaultwarden lauscht im Container auf <strong>Port 80</strong>.
Diese Zeile sagt Traefik explizit, wohin es intern weiterleiten soll, statt den
Zielport aus dem Image raten zu lassen – dasselbe Muster wie bei jeder App hinter
Traefik.</li>
<li><strong>Kein <code>ports:</code></strong> – wie bei jeder App hinter Traefik ist Vaultwarden nur über den
Proxy erreichbar, niemals direkt von außen.</li>
</ul>
<h3 id="schritt-3-starten-und-den-ersten-aufruf">Schritt 3: Starten und den ersten Aufruf</h3>
<p>Starte den 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">docker compose up -d</span></span></code></pre></div>
</div>
<p>Beim ersten Start legt Vaultwarden das Datenverzeichnis an und initialisiert die
Datenbank. Wirf einen Blick ins Log:</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 logs -f vaultwarden</span></span></code></pre></div>
</div>
<p>Du solltest am Ende diese Zeile sehen – sie bestätigt, dass der Dienst läuft:</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">[INFO] Rocket has launched from http://0.0.0.0:80</span></span></code></pre></div>
</div>
<p>Mit <code>Strg+C</code> verlässt du die Log-Ansicht wieder (der Container läuft weiter). Ruf jetzt
<code>https://vault.DEINE_DOMAIN</code> im Browser auf. Traefik holt beim ersten Zugriff das
Let&rsquo;s-Encrypt-Zertifikat – das kann ein paar Sekunden dauern. Danach erscheint die
Bitwarden-Web-Oberfläche.</p>
<p>Klick auf <strong>Konto erstellen</strong> und leg dein erstes Konto an – mit deiner E-Mail-Adresse
und einem Namen:</p>
<p><figure class="my-6"><img src="/tutorials/vaultwarden-passwortmanager/vaultwarden-konto-erstellen_hu_4a122f72dc709159.webp" srcset="/tutorials/vaultwarden-passwortmanager/vaultwarden-konto-erstellen_hu_38c8998945a4d549.webp 480w, /tutorials/vaultwarden-passwortmanager/vaultwarden-konto-erstellen_hu_4a122f72dc709159.webp 768w, /tutorials/vaultwarden-passwortmanager/vaultwarden-konto-erstellen_hu_874c7bf999471364.webp 1200w, /tutorials/vaultwarden-passwortmanager/vaultwarden-konto-erstellen_hu_f44917561bbcc1d1.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/vaultwarden-passwortmanager/vaultwarden-konto-erstellen_hu_a7bbe3db7cd7fb21.webp"
    alt="Das Vaultwarden-Web-Interface zeigt das Formular zum Erstellen eines neuen Kontos mit E-Mail-Adresse und Name" title="Konto erstellen – Vaultwarden nutzt die offizielle Bitwarden-Web-Oberfläche"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Konto erstellen – Vaultwarden nutzt die offizielle Bitwarden-Web-Oberfläche</figcaption></figure></p>
<p>Im nächsten Schritt legst du dein <strong>Master-Passwort</strong> fest. Das ist der eine
Schlüssel, der deinen gesamten Tresor entsperrt:</p>
<p><figure class="my-6"><img src="/tutorials/vaultwarden-passwortmanager/vaultwarden-master-passwort_hu_849cc40e7b1cd644.webp" srcset="/tutorials/vaultwarden-passwortmanager/vaultwarden-master-passwort_hu_d511b12d9d26dc4a.webp 480w, /tutorials/vaultwarden-passwortmanager/vaultwarden-master-passwort_hu_849cc40e7b1cd644.webp 768w, /tutorials/vaultwarden-passwortmanager/vaultwarden-master-passwort_hu_9991a23fb3f21fe9.webp 1200w, /tutorials/vaultwarden-passwortmanager/vaultwarden-master-passwort_hu_e17e3e4cebab462b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/vaultwarden-passwortmanager/vaultwarden-master-passwort_hu_11fe2cf7874e5264.webp"
    alt="Die Vaultwarden-Seite zum Festlegen eines starken Master-Passworts mit Passwortfeld und Stärke-Anzeige" title="Das Master-Passwort verschlüsselt deinen gesamten Tresor – wähle es mit Bedacht"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Master-Passwort verschlüsselt deinen gesamten Tresor – wähle es mit Bedacht</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>Das Master-Passwort ist nicht wiederherstellbar
  </p>
  <div class="prose-kitchen text-sm">Dein Master-Passwort wird <strong>nie</strong> an den Server übertragen; es entschlüsselt deinen
Tresor lokal. Vergisst du es, sind <strong>alle</strong> darin gespeicherten Passwörter verloren –
niemand, auch du selbst nicht, kann sie wiederherstellen. Wähle eine lange, einmalige
Passphrase und hinterlege sie an einem sicheren Ort (z. B. ausgedruckt im Safe).</div>
</div>
<h3 id="schritt-4-die-registrierung-wieder-schließen">Schritt 4: Die Registrierung wieder schließen</h3>
<p>Sobald dein Konto steht, willst du <strong>verhindern, dass Fremde sich ebenfalls
registrieren</strong> können – dein Vaultwarden hängt schließlich offen im Internet. Setz
<code>SIGNUPS_ALLOWED</code> in der <code>compose.yaml</code> auf <code>false</code>:</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">DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://vault.DEINE_DOMAIN&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SIGNUPS_ALLOWED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ADMIN_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;$$argon2id$$...&#34;</span></span></span></code></pre></div>
</div>
<p>Und übernimm die Änderung:</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></code></pre></div>
</div>
<p>Compose erkennt die geänderte Umgebungsvariable und startet den Container neu. Ab jetzt
weist die Anmeldeseite neue Registrierungen ab. Weitere Nutzer (z. B. für die Familie)
lädst du bei Bedarf gezielt über das Admin-Panel ein – dazu gleich mehr.</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>Nicht vergessen
  </p>
  <div class="prose-kitchen text-sm">Ein offenes <code>SIGNUPS_ALLOWED: &quot;true&quot;</code> ist die häufigste Fehlkonfiguration bei
selbst gehostetem Vaultwarden. Jeder, der deine Domain kennt, könnte sich sonst ein
Konto anlegen. Dieser Schritt ist Pflicht, kein Extra.</div>
</div>
<h3 id="schritt-5-das-admin-panel">Schritt 5: Das Admin-Panel</h3>
<p>Ruf <code>https://vault.DEINE_DOMAIN/admin</code> auf und melde dich mit dem <strong>Passwort</strong> an, das
du in Schritt 1 beim Erzeugen des Hashes eingegeben hast (nicht mit dem Hash selbst).
Du landest in der Verwaltungsoberfläche:</p>
<p><figure class="my-6"><img src="/tutorials/vaultwarden-passwortmanager/vaultwarden-admin_hu_473707ac677392b3.webp" srcset="/tutorials/vaultwarden-passwortmanager/vaultwarden-admin_hu_9aeb46075faece5c.webp 480w, /tutorials/vaultwarden-passwortmanager/vaultwarden-admin_hu_473707ac677392b3.webp 768w, /tutorials/vaultwarden-passwortmanager/vaultwarden-admin_hu_5667882637e7d363.webp 1200w, /tutorials/vaultwarden-passwortmanager/vaultwarden-admin_hu_d5ec00afc73e4d04.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/vaultwarden-passwortmanager/vaultwarden-admin_hu_6e7aad8d3eec6a20.webp"
    alt="Das Vaultwarden-Admin-Panel mit den Bereichen General, SMTP Email und Backup Database" title="Das Admin-Panel – hier verwaltest du Nutzer, SMTP und globale Einstellungen"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Admin-Panel – hier verwaltest du Nutzer, SMTP und globale Einstellungen</figcaption></figure></p>
<p>Hier steuerst du den Dienst zentral:</p>
<ul>
<li><strong>Users</strong> – vorhandene Nutzer einsehen, neue <strong>einladen</strong> (auch bei
<code>SIGNUPS_ALLOWED=false</code>), Konten deaktivieren.</li>
<li><strong>Settings → SMTP Email Settings</strong> – trag hier die Zugangsdaten deines Mail-Servers
ein, damit Vaultwarden Einladungen und 2FA-Codes per Mail verschicken kann. Ohne SMTP
funktioniert die Einladung per Link zwar auch, aber E-Mail-basierte 2FA nicht.</li>
<li><strong>Diagnostics</strong> – zeigt dir, ob deine <code>DOMAIN</code> korrekt gesetzt ist und ob Vaultwarden
von außen erreichbar ist – praktisch bei der Fehlersuche.</li>
</ul>
<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>Einstellungen im Panel vs. Umgebungsvariablen
  </p>
  <div class="prose-kitchen text-sm">Was du im Admin-Panel speicherst, wird in <code>vw-data/config.json</code> abgelegt und
<strong>überschreibt</strong> die Umgebungsvariablen aus der <code>compose.yaml</code>. Entscheide dich pro
Einstellung für einen Weg, um Verwirrung zu vermeiden. Für die Grund-Konfiguration
(Domain, Token, Signups) bleiben wir bewusst bei der Compose-Datei – sie ist
versionierbar und reproduzierbar.</div>
</div>
<h3 id="schritt-6-die-clients-verbinden">Schritt 6: Die Clients verbinden</h3>
<p>Jetzt kommt der eigentliche Nutzen. Installiere die <strong>offizielle Bitwarden-App</strong> oder
-Browser-Erweiterung (aus dem jeweiligen App- bzw. Add-on-Store). Bevor du dich
anmeldest, stellst du den Server um:</p>
<ol>
<li>Öffne in der App/Erweiterung <strong>noch vor dem Login</strong> die Einstellungen für die
<strong>selbst gehostete Umgebung</strong> (Zahnrad-Symbol bzw. „Region: Selbst gehostet&quot;).</li>
<li>Trag als <strong>Server-URL</strong> <code>https://vault.DEINE_DOMAIN</code> ein und speichere.</li>
<li>Melde dich jetzt mit deiner E-Mail und deinem Master-Passwort an – die App spricht
ab sofort mit deinem Server.</li>
</ol>
<p>Das Schöne: Es sind dieselben ausgereiften Clients wie beim kommerziellen Bitwarden.
Autofill, Passwort-Generator, sichere Notizen, Attachments und 2FA-Speicherung
funktionieren genauso – nur liegen die Daten auf deinem Server.</p>
<p><strong>Bestehende Passwörter importieren:</strong> Wechselst du von einem anderen Manager (oder
dem Browser-Passwortspeicher), musst du nicht alles abtippen. Exportiere dort deine
Einträge als CSV oder JSON und importiere sie über die <strong>Web-Oberfläche</strong> unter
<code>Werkzeuge → Daten importieren</code>. Vaultwarden versteht die Exportformate der gängigen
Manager (KeePass, LastPass, 1Password, Chrome/Firefox u. a.) direkt.</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>Export-Dateien danach sicher löschen
  </p>
  <div class="prose-kitchen text-sm">Eine Export-CSV enthält alle Passwörter <strong>im Klartext</strong>. Lösche die Datei sofort nach
dem erfolgreichen Import wieder – und leere den Papierkorb. Lass sie niemals in
Downloads oder einem Cloud-Ordner liegen.</div>
</div>
<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>Zwei-Faktor-Authentifizierung aktivieren
  </p>
  <div class="prose-kitchen text-sm">Aktiviere in den Kontoeinstellungen deines Tresors die <strong>2FA</strong> (z. B. per
Authenticator-App). Damit ist selbst bei geknacktem Master-Passwort dein Tresor nicht
sofort offen. Für E-Mail-basierte 2FA muss vorher SMTP im Admin-Panel eingerichtet
sein.</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> Der Container startet nicht, das Log sagt sinngemäß
<code>Running without a persistent volume is not recommended</code>.</p>
<p><strong>Ursache &amp; Lösung:</strong> Es fehlt das <code>volumes:</code>-Mapping. Vaultwarden verweigert absichtlich
den Start ohne Datenverzeichnis, damit deine Passwörter nicht in einem flüchtigen
Container landen. Ergänze <code>./vw-data:/data</code> wie in Schritt 2 und starte neu.</p>
<p><strong>Symptom:</strong> Die Web-Oberfläche zeigt „<strong>You need to enable HTTPS!</strong>&quot; oder der Login
scheitert mit Krypto-Fehlern.</p>
<p><strong>Ursache &amp; Lösung:</strong> Vaultwarden nutzt die Web-Crypto-API des Browsers, die nur in
einem <strong>sicheren Kontext</strong> (echtes HTTPS) verfügbar ist. Du hast die Seite über <code>http://</code>
oder mit ungültigem Zertifikat aufgerufen. Stell sicher, dass Traefik ein gültiges
Let&rsquo;s-Encrypt-Zertifikat geholt hat (Traefik-Log prüfen) und du die Seite über
<code>https://</code> erreichst. Die <code>DOMAIN</code>-Variable muss ebenfalls mit <code>https://</code> beginnen.</p>
<p><strong>Symptom:</strong> Das Admin-Panel weist dein Passwort ab, obwohl es stimmt.</p>
<p><strong>Ursache &amp; Lösung:</strong> Vermutlich sind die Dollarzeichen im <code>ADMIN_TOKEN</code> nicht
verdoppelt. In der <code>compose.yaml</code> muss aus jedem <code>$</code> ein <code>$$</code> werden. Prüfe mit
<code>docker compose config</code>, wie der Token tatsächlich ankommt (Compose zeigt dort den
aufgelösten Wert). Denk auch daran: Beim Login gibst du das <strong>Passwort</strong> ein, nicht den
Hash.</p>
<p><strong>Symptom:</strong> Die Handy-App findet den Server nicht oder meldet „Server-URL ungültig&quot;.</p>
<p><strong>Ursache &amp; Lösung:</strong> Die Server-URL muss die vollständige <code>https://</code>-Adresse ohne
abschließenden Pfad sein (<code>https://vault.DEINE_DOMAIN</code>). Prüfe außerdem, ob die Domain
von außen per Browser erreichbar ist und das Zertifikat gültig ist – Apps sind bei
Zertifikatsfehlern strenger als Browser.</p>
<p><strong>Symptom:</strong> Trotz <code>SIGNUPS_ALLOWED=false</code> konnte sich jemand registrieren.</p>
<p><strong>Ursache &amp; Lösung:</strong> Wahrscheinlich wurde die Einstellung im Admin-Panel gesetzt und
überschreibt die Umgebungsvariable, oder der Container wurde nach der Änderung nicht neu
gestartet. Kontrolliere den Wert unter <strong>Settings → General settings</strong> im Panel und
starte mit <code>docker compose up -d</code> neu.</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li>
<p><strong>Backups sind bei einem Passwortmanager nicht verhandelbar.</strong> Dein gesamter Tresor
steckt im Verzeichnis <code>vw-data/</code> (SQLite-Datenbank, Keys, Attachments). Sichere es
<strong>verschlüsselt und off-site</strong> mit <a href="/tutorials/backups-mit-restic/">Restic</a> – nimm den
Ordner <code>~/vaultwarden/vw-data</code> in deine Backup-Quellen auf. Für einen konsistenten
Datenbank-Stand führst du vor dem Backup kurz <code>docker compose stop</code> aus oder nutzt die
„Backup Database&quot;-Funktion im Admin-Panel. Eine bloße Dateikopie bei <strong>laufendem</strong>
Container kann dagegen einen inkonsistenten Stand erwischen (SQLite schreibt in
WAL-Dateien) – bei einem Passwortmanager ist das kein Risiko wert.</p>
</li>
<li>
<p><strong>Updates kontrolliert einspielen.</strong> Weil wir die Version gepinnt haben, aktualisierst
du bewusst: Vor dem Wechsel die
<a href="https://github.com/dani-garcia/vaultwarden/releases">Release-Notes</a> lesen, dann den
Tag in der <code>compose.yaml</code> erhöhen (z. B. auf die nächste <code>1.x</code>) und neu ziehen:</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 pull <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p>Prüfe danach im Log wieder die <code>Rocket has launched</code>-Zeile und teste einen Login.</p>
</li>
<li>
<p><strong>Registrierung geschlossen lassen.</strong> Kontrolliere gelegentlich, dass
<code>SIGNUPS_ALLOWED</code> weiterhin <code>false</code> ist – neue Nutzer immer gezielt über das
Admin-Panel einladen.</p>
</li>
<li>
<p><strong>ADMIN_TOKEN rotieren</strong>, wenn er kompromittiert sein könnte: neuen Hash mit dem
<code>hash</code>-Befehl aus Schritt 1 erzeugen, in der <code>compose.yaml</code> ersetzen, neu starten.</p>
</li>
<li>
<p><strong>Master-Passwort niemals verlieren.</strong> Es gibt keine Wiederherstellung. Bei einem
Team/Familien-Setup kann ein Notfall-Zugang (Emergency Access) sinnvoll sein – den
richtest du pro Tresor in den Kontoeinstellungen ein.</p>
</li>
</ul>
]]></content:encoded></item><item><title>Traefik einrichten: Reverse Proxy mit automatischem HTTPS</title><link>https://serverkueche.de/tutorials/reverse-proxy-traefik/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/reverse-proxy-traefik/</guid><description>Traefik als Reverse Proxy vor deinen Containern, mit automatischen Let's-Encrypt-Zertifikaten: Jede App bekommt Domain und HTTPS über ein paar Labels.</description><content:encoded><![CDATA[<p>Das hier ist der wichtigste Baustein der Serverküche. Ein <strong>Reverse Proxy</strong> nimmt
alle Anfragen auf den Ports 80 und 443 entgegen und verteilt sie anhand der Domain
an den richtigen Container – und <strong>Traefik</strong> holt dabei die HTTPS-Zertifikate
vollautomatisch von Let&rsquo;s Encrypt. Ab hier bekommt jede weitere App ihre Domain und
ihr TLS mit wenigen Zeilen Labels, ohne dass du je wieder ein Zertifikat von Hand
anfasst.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Traefik v3</strong> als zentraler Einstiegspunkt auf deinem Server. Es
lauscht auf Port 80/443, erkennt neue Container automatisch über Docker-Labels,
leitet HTTP automatisch auf HTTPS um und besorgt für jede Domain ein gültiges
<strong>Let&rsquo;s-Encrypt-Zertifikat</strong>. Als erste App hängen wir <code>whoami</code> hinter den Proxy –
ein winziger Testdienst, der zeigt, dass Routing und TLS greifen. Ein abgesichertes
Dashboard gibt es obendrauf.</p>
<p>Das Muster aus diesem Tutorial – ein gemeinsames <code>proxy</code>-Netzwerk plus ein paar
Labels – wiederholt sich danach in <strong>jedem</strong> App-Rezept.</p>
<p>Eine Anfrage durchläuft in Traefik immer dieselben vier Stationen – dieses
Vokabular hilft dir beim Debuggen:</p>
<ol>
<li><strong>Entrypoint</strong> – der Port, auf dem die Anfrage ankommt (<code>web</code> = 80,
<code>websecure</code> = 443).</li>
<li><strong>Router</strong> – entscheidet anhand einer <strong>Regel</strong> (meist <code>Host(...)</code>), ob diese
Anfrage zu einer App gehört.</li>
<li><strong>Middleware</strong> <em>(optional)</em> – verändert die Anfrage unterwegs (z. B.
HTTPS-Redirect, Basic-Auth, Security-Header).</li>
<li><strong>Service</strong> – der Container, der am Ende antwortet.</li>
</ol>
<p>Merksatz zum Debuggen: „<strong>Entrypoint → Router → Middleware → Service</strong>&quot;. Landet eine
Anfrage im Nichts, ist fast immer der Router (falsche Domain) oder das Netzwerk
(Service nicht erreichbar) schuld.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><a href="/tutorials/docker-installieren/">Docker + Compose installiert</a> und die
<a href="/tutorials/docker-compose-grundlagen/">Compose-Grundlagen</a> verstanden</li>
<li>Eine <a href="/tutorials/domain-mit-server-verbinden/">mit dem Server verbundene Domain</a>:
<code>DEINE_DOMAIN</code> und die Subdomains müssen per A/AAAA auf den Server auflösen</li>
<li><strong>Port 80 und 443 sind aus dem Internet erreichbar</strong> – Let&rsquo;s Encrypt prüft
darüber, dass dir die Domain gehört. Firewalls entsprechend öffnen (siehe
<a href="/tutorials/firewall-ufw-einrichten/">Firewall mit UFW einrichten</a>).</li>
</ul>
<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>Ohne auflösende Domain kein Zertifikat
  </p>
  <div class="prose-kitchen text-sm">Let&rsquo;s Encrypt stellt nur Zertifikate für Domains aus, die es erreichen kann. Prüfe
<strong>vorher</strong> mit <code>dig +short DEINE_DOMAIN</code>, dass deine Server-IP zurückkommt. Zeigt der
Record noch ins Leere, scheitert die Zertifikatsausstellung – das ist der häufigste
Traefik-Fehler überhaupt.</div>
</div>
<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-das-gemeinsame-proxy-netzwerk-anlegen">Schritt 1: Das gemeinsame proxy-Netzwerk anlegen</h3>
<p>Traefik und alle Apps müssen sich ein Docker-Netzwerk teilen, damit Traefik die
Container erreicht. Wir legen es <strong>einmalig</strong> und explizit an, damit spätere Stacks
einfach andocken können:</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 network create proxy</span></span></code></pre></div>
</div>
<p>Prüfe:</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 network ls <span class="p">|</span> grep proxy</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">c442da869c47   proxy     bridge    local</span></span></code></pre></div>
</div>
<p>Dieses Netzwerk ist von den einzelnen Compose-Projekten unabhängig – deshalb binden
wir es später als <code>external</code> ein.</p>
<h3 id="schritt-2-traefik-projekt-anlegen">Schritt 2: Traefik-Projekt anlegen</h3>
<p>Lege einen eigenen Ordner für Traefik an und darin die Datei, in der die
Zertifikate gespeichert werden:</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 ~/traefik <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/traefik
</span></span><span class="line"><span class="cl">touch acme.json
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> acme.json</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>acme.json braucht 600
  </p>
  <div class="prose-kitchen text-sm">Ohne <code>chmod 600 acme.json</code> <strong>überspringt Traefik den Let&rsquo;s-Encrypt-Resolver</strong>: Der
Container startet zwar, stellt aber kein gültiges Zertifikat aus – du landest auf
Traefiks selbstsigniertem Notfall-Zertifikat. Im Log steht dann <code>permissions 644 for /acme.json are too open, please use 600</code>. Die Datei enthält deine privaten Schlüssel
– nur der Besitzer darf sie lesen.</div>
</div>
<h3 id="schritt-3-die-traefik-composeyaml">Schritt 3: Die Traefik-compose.yaml</h3>
<p>Jetzt die zentrale Konfiguration. Sie ist lang, aber jede Zeile hat einen Zweck –
die Erklärung folgt direkt darunter:</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="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">traefik</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">traefik:v3.7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Dashboard (in Schritt 7 abgesichert)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--api.dashboard=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Docker als Quelle; nur Container mit traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--providers.docker=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--providers.docker.exposedbydefault=false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--providers.docker.network=proxy&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Einstiegspunkte: 80 (HTTP) und 443 (HTTPS)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--entrypoints.web.address=:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--entrypoints.websecure.address=:443&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Alles von HTTP automatisch auf HTTPS umleiten</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--entrypoints.web.http.redirections.entrypoint.to=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--entrypoints.web.http.redirections.entrypoint.scheme=https&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Let&#39;s-Encrypt-Resolver namens &#34;le&#34; via HTTP-Challenge</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--certificatesresolvers.le.acme.email=DEINE_MAIL&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--certificatesresolvers.le.acme.storage=/acme.json&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--certificatesresolvers.le.acme.httpchallenge=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--certificatesresolvers.le.acme.httpchallenge.entrypoint=web&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;80:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;443:443&#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">/var/run/docker.sock:/var/run/docker.sock:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./acme.json:/acme.json</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></span><span class="line"><span class="cl"><span class="w">      </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">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></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">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 wichtigsten Blöcke:</p>
<ul>
<li><strong><code>providers.docker</code> + <code>exposedbydefault=false</code></strong>: Traefik beobachtet den
Docker-Socket, aber nur Container, die ausdrücklich <code>traefik.enable=true</code> tragen.
Kein Dienst wird versehentlich öffentlich.</li>
<li><strong><code>providers.docker.network=proxy</code></strong>: sagt Traefik, über welches Netzwerk es die
Container anspricht – wichtig, wenn Container in mehreren Netzen hängen.</li>
<li><strong><code>entrypoints web/websecure</code></strong>: die Ports 80 und 443. Die beiden
<code>redirections</code>-Zeilen schicken jeden HTTP-Aufruf automatisch nach HTTPS.</li>
<li><strong><code>certificatesresolvers.le</code></strong>: der Let&rsquo;s-Encrypt-Resolver. Über die
<strong>HTTP-Challenge</strong> weist Traefik Let&rsquo;s Encrypt nach, dass die Domain auf diesen
Server zeigt, und legt das Zertifikat in <code>acme.json</code> ab. Dafür muss <strong>Port 80 von
außen erreichbar bleiben</strong> – auch wenn deine App nur über HTTPS läuft, denn die
Challenge kommt über HTTP. Die <code>acme.email</code> nutzt Let&rsquo;s Encrypt ausschließlich für
Warnungen zu ablaufenden Zertifikaten; trag eine echte Adresse ein.</li>
<li>Der <strong>Docker-Socket</strong> ist read-only (<code>:ro</code>) eingebunden – Traefik muss ihn lesen,
aber nicht beschreiben.</li>
</ul>
<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>Erst mit dem Staging-Server testen
  </p>
  <div class="prose-kitchen text-sm">Let&rsquo;s Encrypt hat strenge <strong>Rate-Limits</strong> für die Produktiv-Zertifikate. Solange du
noch am Aufbau bist, füge testweise
<code>--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory</code>
hinzu. Das liefert (im Browser als unsicher angezeigte) Test-Zertifikate ohne
Limit. Läuft alles, die Zeile entfernen, <strong><code>acme.json</code> leeren</strong> (<code>&gt; acme.json</code>) und
Traefik neu starten – dann kommt das echte Zertifikat.</div>
</div>
<p>Starte Traefik:</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 logs -f traefik</span></span></code></pre></div>
</div>
<p>In den Logs darf <strong>kein</strong> <code>ERR</code> zu ACME oder zum Provider stehen. <code>Strg+C</code> beendet
nur das Mitlesen, nicht den Container.</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>Leeres Log ist ein gutes Zeichen
  </p>
  <div class="prose-kitchen text-sm">Traefik v3 schreibt bei Standard-Log-Level <strong>nur Fehler</strong>. Eine leere Log-Ausgabe
bedeutet also: alles läuft. Willst du beim Aufbau mehr sehen (jeden erkannten Router,
jede ACME-Anfrage), ergänze <code>--log.level=INFO</code> in den <code>command</code>-Block und starte neu.</div>
</div>
<h3 id="schritt-4-die-erste-app-hinter-traefik-whoami">Schritt 4: Die erste App hinter Traefik (whoami)</h3>
<p><code>whoami</code> ist ein winziger Dienst, der die empfangene Anfrage zurückgibt – perfekt
zum Testen. Eigener Ordner, eigene <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">mkdir -p ~/whoami <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/whoami</span></span></code></pre></div>
</div>
<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="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">whoami</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">traefik/whoami:v1.11</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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.rule=Host(`whoami.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.tls.certresolver=le&#34;</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></span><span class="line"><span class="cl"><span class="w">      </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">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></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">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>Das sind die vier Labels, die du ab jetzt immer wieder brauchst:</p>
<ul>
<li><strong><code>traefik.enable=true</code></strong> – nur dann fasst Traefik den Container an.</li>
<li><strong><code>...routers.whoami.rule=Host(...)</code></strong> – bei welcher Domain dieser Container
antwortet. <code>whoami</code> ist ein frei wählbarer Router-Name (pro Container eindeutig).</li>
<li><strong><code>...entrypoints=websecure</code></strong> – über HTTPS (443) erreichbar.</li>
<li><strong><code>...tls.certresolver=le</code></strong> – hol das Zertifikat über den in Schritt 3
definierten Resolver <code>le</code>.</li>
</ul>
<p>Wichtig: Der Dienst hat <strong>kein <code>ports:</code></strong> – er ist nur über Traefik erreichbar, nicht
direkt von außen. Und er hängt im <strong><code>proxy</code>-Netzwerk</strong>, sonst findet Traefik ihn nicht.</p>
<p>Lege vorher den DNS-Record <code>whoami.DEINE_DOMAIN</code> an (A/AAAA auf die Server-IP,
<a href="/tutorials/domain-mit-server-verbinden/">wie im DNS-Tutorial</a>), dann:</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></code></pre></div>
</div>
<p>Rufe <code>https://whoami.DEINE_DOMAIN</code> im Browser auf. Beim ersten Aufruf braucht die
Zertifikatsausstellung ein paar Sekunden; danach siehst du ein gültiges
Schloss-Symbol und eine Textausgabe wie:</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">Hostname: bfa4b8dee3d0
</span></span><span class="line"><span class="cl">IP: 127.0.0.1
</span></span><span class="line"><span class="cl">IP: 172.19.0.3
</span></span><span class="line"><span class="cl">RemoteAddr: 172.19.0.2:49734
</span></span><span class="line"><span class="cl">GET / HTTP/1.1
</span></span><span class="line"><span class="cl">Host: whoami.DEINE_DOMAIN</span></span></code></pre></div>
</div>
<p>Die Zeile <code>Host:</code> bestätigt, dass Traefik anhand der Domain korrekt an diesen
Container geroutet hat. Genau dieses Verhalten – Anfrage für <code>whoami.DEINE_DOMAIN</code>
landet beim whoami-Container, eine fremde Domain bekommt <strong>404</strong> – ist das Herz des
Reverse Proxys.</p>
<p><strong>Prüfe, welche CA das Zertifikat ausgestellt hat.</strong> Das trennt „HTTPS läuft&quot; von
„ich sehe nur Traefiks Notfall-Zertifikat&quot;:</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">echo</span> <span class="p">|</span> openssl s_client -connect whoami.DEINE_DOMAIN:443 -servername whoami.DEINE_DOMAIN 2&gt;/dev/null <span class="p">|</span> openssl x509 -noout -issuer</span></span></code></pre></div>
</div>
<p>Solange du (wie in Schritt 3 empfohlen) den <strong>Staging</strong>-Server nutzt, steht dort ein
Test-Aussteller – der Browser zeigt das Zertifikat noch als unsicher an:</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">issuer=C=US, O=Let&#39;s Encrypt, CN=(STAGING) Ersatz Emmer YR2</span></span></code></pre></div>
</div>
<p>Kommt hier <code>TRAEFIK DEFAULT CERT</code>, hat der Resolver kein Zertifikat geholt – dann
zur Fehlersuche unten. Steht ein Let&rsquo;s-Encrypt-Aussteller da, funktioniert die
komplette Kette.</p>
<h3 id="schritt-5-auf-das-echte-zertifikat-umschalten">Schritt 5: Auf das echte Zertifikat umschalten</h3>
<p>Sobald Staging sauber läuft, holst du das richtige, im Browser gültige Zertifikat.
Entferne die <code>caserver</code>-Zeile aus dem <code>traefik</code>-Service (Schritt 3), <strong>leere die
Staging-Zertifikate</strong> und starte Traefik neu:</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">&gt; acme.json                 <span class="c1"># verwirft die Staging-Zertifikate (chmod 600 bleibt)</span>
</span></span><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div>
</div>
<p>Beim nächsten Aufruf holt Traefik ein frisches Produktiv-Zertifikat. In der
<code>issuer</code>-Zeile von oben verschwindet danach das <code>(STAGING)</code>, und der Browser zeigt
ein gültiges Schloss.</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>Erst Staging, dann Produktion
  </p>
  <div class="prose-kitchen text-sm">Let&rsquo;s Encrypt hat harte <strong>Rate-Limits</strong> auf Produktiv-Zertifikate (wenige pro Domain
und Woche). Wechsle erst auf Produktion, wenn Routing und Challenge mit Staging
nachweislich klappen – sonst sperrst du dir die Domain für Stunden aus.</div>
</div>
<h3 id="schritt-6-der-http-nach-https-redirect">Schritt 6: Der HTTP-nach-HTTPS-Redirect</h3>
<p>Den hast du in Schritt 3 schon global aktiviert (die zwei <code>redirections</code>-Zeilen).
Test:</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 -sI http://whoami.DEINE_DOMAIN <span class="p">|</span> grep -iE <span class="s1">&#39;HTTP/|location&#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">HTTP/1.1 308 Permanent Redirect
</span></span><span class="line"><span class="cl">location: https://whoami.DEINE_DOMAIN/</span></span></code></pre></div>
</div>
<p>Jeder unverschlüsselte Aufruf wird also automatisch auf HTTPS umgeleitet – du musst
in keiner App mehr daran denken.</p>
<h3 id="schritt-7-das-dashboard-absichern">Schritt 7: Das Dashboard absichern</h3>
<p>Traefik bringt ein Dashboard mit, das zeigt, welche Router und Services aktiv sind.
Es <strong>nie ungeschützt</strong> ins Internet stellen. Wir sichern es mit Basic-Auth und
hängen es an eine eigene Subdomain. Erzeuge zuerst einen Benutzer:</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">sudo apt install -y apache2-utils
</span></span><span class="line"><span class="cl">htpasswd -nbB admin DEIN_PASSWORT</span></span></code></pre></div>
</div>
<p>Die Ausgabe (<code>admin:$2y$05$...</code>) kommt in die Labels. <strong>In der <code>compose.yaml</code> jedes
<code>$</code> verdoppeln</strong> (<code>$$</code>), sonst interpretiert Compose es als Variable. Ergänze im
<code>traefik</code>-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="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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dashboard.rule=Host(`traefik.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dashboard.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dashboard.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dashboard.service=api@internal&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dashboard.middlewares=dashboard-auth&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$...&#34;</span></span></span></code></pre></div>
</div>
<p>Nach <code>docker compose up -d</code> erreichst du das Dashboard unter
<code>https://traefik.DEINE_DOMAIN</code> – nach Passwortabfrage.</p>
<p>Im Dashboard siehst du unter <strong>HTTP → Routers</strong> jeden erkannten Router (mit seiner
<code>Host(...)</code>-Regel), unter <strong>Services</strong> die dahinterliegenden Container und unter
<strong>Middlewares</strong> deine Bausteine wie <code>dashboard-auth</code>. Ein Router wird <strong>grün</strong>, wenn
Regel, Service und – bei <code>websecure</code> – das Zertifikat stimmen; <strong>rot</strong> heißt, dass
etwas fehlt (meist Netzwerk oder Host-Regel). Damit ist das Dashboard dein erster
Blick bei „warum antwortet meine App nicht?&quot;.</p>
<p><figure class="my-6"><img src="/tutorials/reverse-proxy-traefik/traefik-dashboard_hu_5849a1b2dfad3fb8.webp" srcset="/tutorials/reverse-proxy-traefik/traefik-dashboard_hu_9cab1a77d2d2e383.webp 480w, /tutorials/reverse-proxy-traefik/traefik-dashboard_hu_5849a1b2dfad3fb8.webp 768w, /tutorials/reverse-proxy-traefik/traefik-dashboard_hu_477dc2b839a1268b.webp 1200w, /tutorials/reverse-proxy-traefik/traefik-dashboard_hu_6912160ac6e2d97f.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/reverse-proxy-traefik/traefik-dashboard_hu_5dfd90f74c454165.webp"
    alt="Das Traefik-Dashboard: Entrypoints (web/websecure), erkannte HTTP-Router und Services – alle grün" title="Das Traefik-Dashboard im Überblick"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Traefik-Dashboard im Überblick</figcaption></figure></p>
<p>Unter <strong>HTTP Routers</strong> siehst du jeden Router einzeln – mit seiner <code>Host(...)</code>-Regel,
dem Entrypoint, dem TLS-Status (Schloss) und dem Provider <code>docker</code>. So prüfst du auf
einen Blick, ob deine Labels korrekt erkannt wurden:</p>
<p><figure class="my-6"><img src="/tutorials/reverse-proxy-traefik/traefik-routers_hu_2424c2a45482f193.webp" srcset="/tutorials/reverse-proxy-traefik/traefik-routers_hu_6a794561852d450.webp 480w, /tutorials/reverse-proxy-traefik/traefik-routers_hu_2424c2a45482f193.webp 768w, /tutorials/reverse-proxy-traefik/traefik-routers_hu_cbd2f006d49919b9.webp 1200w, /tutorials/reverse-proxy-traefik/traefik-routers_hu_1ef1bcad6b9c811.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/reverse-proxy-traefik/traefik-routers_hu_37c445c54306ba89.webp"
    alt="Die Router-Liste im Traefik-Dashboard: pro App die Host-Regel, der Entrypoint (websecure), TLS und der Docker-Provider" title="HTTP-Router mit Host-Regeln, Entrypoint und Provider"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">HTTP-Router mit Host-Regeln, Entrypoint und Provider</figcaption></figure></p>
<h3 id="schritt-8-sicherheits-header-als-wiederverwendbare-middleware">Schritt 8: Sicherheits-Header als wiederverwendbare Middleware</h3>
<p>Eine <strong>Middleware</strong> hängt sich zwischen Router und Service und verändert die
Anfrage oder Antwort. Ein Set an Sicherheits-Headern gehört auf jede öffentliche
App – einmal definiert, überall angehängt. Definiere die Middleware an einem
beliebigen Container (üblich: bei Traefik selbst) über Labels:</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="s2">&#34;traefik.http.middlewares.sec-headers.headers.stsSeconds=31536000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.sec-headers.headers.stsIncludeSubdomains=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.sec-headers.headers.frameDeny=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.sec-headers.headers.browserXssFilter=true&#34;</span></span></span></code></pre></div>
</div>
<p>Was die wichtigsten bewirken:</p>
<ul>
<li><strong><code>stsSeconds</code> (HSTS)</strong> – der Browser spricht die Domain künftig nur noch über
HTTPS an. Ein Jahr (<code>31536000</code>) ist der übliche Wert.</li>
<li><strong><code>frameDeny</code></strong> – verbietet das Einbetten in fremde <code>&lt;iframe&gt;</code>s (Clickjacking-Schutz).</li>
<li><strong><code>contentTypeNosniff</code></strong> – der Browser rät den Content-Type nicht, sondern nimmt
den gelieferten – schließt eine ganze Klasse von Angriffen aus.</li>
</ul>
<p>Anhängen an eine App per Label (Router-Name anpassen):</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="s2">&#34;traefik.http.routers.whoami.middlewares=sec-headers&#34;</span></span></span></code></pre></div>
</div>
<p>Mehrere Middlewares werden komma-getrennt angegeben (<code>sec-headers,dashboard-auth</code>)
und <strong>in dieser Reihenfolge</strong> durchlaufen. So baust du dir nach und nach einen
Baukasten (Auth, Rate-Limiting, IP-Whitelist), den jede App wiederverwenden kann.</p>
<p>Soll ein Header-Set <strong>für alle</strong> Apps gelten, hängst du die Middleware nicht an jeden
Router einzeln, sondern global an den Entrypoint – eine Zeile im <code>command</code>-Block von
Traefik:</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="s2">&#34;--entrypoints.websecure.http.middlewares=sec-headers@docker&#34;</span></span></span></code></pre></div>
</div>
<p>Der Zusatz <code>@docker</code> sagt Traefik, dass die Middleware aus dem Docker-Provider stammt
(dort hast du sie per Label definiert).</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>Tipp
  </p>
  <div class="prose-kitchen text-sm">HSTS mit <code>stsSeconds</code> erst scharf schalten, wenn HTTPS <strong>sicher</strong> und dauerhaft
läuft. Der Browser merkt sich die Vorgabe hartnäckig – ein kaputtes Zertifikat wäre
dann für die volle Dauer nur schwer zu umgehen.</div>
</div>
<h3 id="schritt-9-das-rezept-für-jede-weitere-app">Schritt 9: Das Rezept für jede weitere App</h3>
<p>Ab jetzt ist jede App dasselbe Muster – du brauchst Traefik nie wieder anzufassen.
Eine neue Anwendung bekommt einen eigenen Ordner mit einer <code>compose.yaml</code>, hängt im
<code>proxy</code>-Netzwerk und trägt genau diese Labels (Router-Name und Domain anpassen, bei
einem Port ≠ 80 zusätzlich das <code>loadbalancer</code>-Label):</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="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">meineapp</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">DEIN_IMAGE:TAG</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="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.meineapp.rule=Host(`app.DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.meineapp.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.meineapp.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># nur nötig, wenn die App NICHT auf Port 80 lauscht:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.meineapp.loadbalancer.server.port=DEIN_PORT&#34;</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></span><span class="line"><span class="cl"><span class="w">      </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">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></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">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><code>docker compose up -d</code>, DNS-Record auf die Server-IP setzen, fertig – Domain und
HTTPS entstehen automatisch. Genau so hängt
<a href="/tutorials/uptime-kuma-monitoring/">die erste echte App (Uptime Kuma)</a> hinter
den Proxy.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<p><strong>Symptom:</strong> Im Browser „Zertifikat ungültig&quot; oder Traefik-Log zeigt ACME-Fehler.</p>
<p><strong>Ursache &amp; Lösung:</strong> Die drei üblichen Gründe: (1) Der DNS-Record zeigt noch nicht
auf den Server – <code>dig +short DEINE_DOMAIN</code> prüfen. (2) Port 80 ist von außen nicht
erreichbar (Firewall/netcup-Firewall) – die HTTP-Challenge braucht ihn. (3) Du hast
das <strong>Rate-Limit</strong> der Produktiv-CA gerissen – auf den Staging-Server wechseln
(Tipp in Schritt 3), testen, dann zurück.</p>
<p><strong>Symptom:</strong> <code>404 page not found</code> beim Aufruf der App-Domain.</p>
<p><strong>Ursache &amp; Lösung:</strong> Traefik kennt die Route nicht. Prüfe: Hat der Container
<code>traefik.enable=true</code>? Hängt er im <strong><code>proxy</code>-Netzwerk</strong>? Stimmt die Domain in der
<code>Host(...)</code>-Regel exakt (inkl. Subdomain)? Das Dashboard (Schritt 7) zeigt unter
„HTTP Routers&quot;, ob der Router registriert wurde.</p>
<p><strong>Symptom:</strong> Der Browser zeigt Traefiks selbstsigniertes Notfall-Zertifikat; im Log
steht <code>permissions 644 for /acme.json are too open, please use 600</code>.</p>
<p><strong>Ursache &amp; Lösung:</strong> Traefik läuft, hat aber den ACME-Resolver übersprungen – daher
kein echtes Zertifikat. <code>chmod 600 acme.json</code> nachholen (Schritt 2) und Container neu
starten.</p>
<p><strong>Symptom:</strong> Keine App wird geroutet; im Traefik-Log wiederholt sich
<code>client version 1.24 is too old. Minimum supported API version is 1.40</code>.</p>
<p><strong>Ursache &amp; Lösung:</strong> Deine Traefik-Version ist zu alt für deine Docker-Engine – der
Docker-Provider kann den Socket nicht mehr abfragen. Aktuelles Docker (Engine 29,
API-Level ≥ 1.40) braucht <strong>Traefik ≥ v3.5</strong>; deshalb nutzt dieses Tutorial
<code>traefik:v3.7</code>. Ältere Tags wie <code>v3.3</code> funktionieren mit neuem Docker nicht mehr –
Image-Tag hochziehen und <code>docker compose up -d</code> erneut ausführen.</p>
<p><strong>Symptom:</strong> Basic-Auth am Dashboard wird sofort wieder abgewiesen / Router fehlt.</p>
<p><strong>Ursache &amp; Lösung:</strong> In der <code>compose.yaml</code> müssen die <code>$</code>-Zeichen des Hashes
<strong>verdoppelt</strong> sein (<code>$$</code>). Prüfe den Hash außerhalb noch einmal mit <code>htpasswd -nbB</code>.</p>
<p><strong>Symptom:</strong> <code>Gateway Timeout</code> oder Traefik erreicht den Container nicht.</p>
<p><strong>Ursache &amp; Lösung:</strong> Meist hängt die App im falschen Netzwerk oder Traefik weiß
nicht, welches gemeint ist. <code>providers.docker.network=proxy</code> in Traefik <strong>und</strong>
<code>networks: [proxy]</code> an der App müssen zusammenpassen.</p>
<p><strong>Symptom:</strong> <code>502 Bad Gateway</code>, obwohl der Container läuft.</p>
<p><strong>Ursache &amp; Lösung:</strong> Traefik erreicht den Container, trifft aber den falschen Port.
Lauscht die App nicht auf 80, braucht sie das Label
<code>traefik.http.services.&lt;name&gt;.loadbalancer.server.port=&lt;echter-port&gt;</code>. Genau dieser
Fall begegnet dir bei der ersten App im nächsten Tutorial (Uptime Kuma auf 3001).</p>
<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li><strong>Sichern musst du <code>acme.json</code> und alle <code>compose.yaml</code>.</strong> Damit ist Traefik nach
einem Crash in Minuten wiederhergestellt – die Zertifikate müssen nicht neu
ausgestellt werden (schont auch das Rate-Limit). Ein verschlüsseltes Off-Site-
Backup dieser Dateien bauen wir im
<a href="/tutorials/backups-mit-restic/">Restic-Tutorial</a>.</li>
<li><strong>Zertifikate erneuern sich automatisch.</strong> Let&rsquo;s-Encrypt-Zertifikate laufen nach
90 Tagen ab; Traefik verlängert sie rechtzeitig von selbst – kein Cronjob nötig.
Kontrollieren kannst du das Ablaufdatum jederzeit, indem du an den <code>openssl</code>-Befehl
aus Schritt 4 statt <code>-issuer</code> einfach <code>-dates</code> hängst (zeigt <code>notBefore</code>/<code>notAfter</code>).</li>
<li><strong>Traefik-Version pflegen.</strong> Der feste Tag (<code>traefik:v3.7</code>) bedeutet: Updates
spielst du bewusst ein. Vor einem Sprung auf eine neue Minor-/Major-Version die
Release-Notes lesen – Traefik hat zwischen v2 und v3 z. B. Label-Syntax geändert.</li>
<li><strong>Dashboard im Blick behalten.</strong> Ein kurzer Login zeigt, ob alle Router „grün&quot;
sind – die schnellste Kontrolle, ob nach einem Deploy alles hängt.</li>
</ul>
<p>Ab jetzt ist der Weg für jede App gleich: Container ins <code>proxy</code>-Netzwerk, vier
Labels dran, DNS-Record setzen – fertig ist ein öffentlich erreichbarer Dienst mit
HTTPS. Als <strong>erste echte App</strong> hängen wir im nächsten Rezept <strong>Uptime Kuma</strong> hinter
Traefik und überwachen damit alle folgenden Dienste.</p>
]]></content:encoded></item></channel></rss>