<?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>Reverse-Proxy – Serverküche</title><link>https://serverkueche.de/tags/reverse-proxy/</link><description>Reverse-Proxy – 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>Sat, 18 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/reverse-proxy/index.xml" rel="self" type="application/rss+xml"/><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>