<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://docs.pazdzewicz.de/feed.xml" rel="self" type="application/atom+xml" /><link href="https://docs.pazdzewicz.de/" rel="alternate" type="text/html" /><updated>2026-03-25T15:09:05+00:00</updated><id>https://docs.pazdzewicz.de/feed.xml</id><title type="html">Pazdzewicz.de Dokumentation</title><subtitle>Allerlei Dokumentation der Pazdzewicz.de</subtitle><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><entry><title type="html">Modernes System-Design einer hochskalierbaren Anwendung</title><link href="https://docs.pazdzewicz.de/devops/2025/12/03/modern-system-design.html" rel="alternate" type="text/html" title="Modernes System-Design einer hochskalierbaren Anwendung" /><published>2025-12-03T12:00:00+00:00</published><updated>2025-12-03T12:00:00+00:00</updated><id>https://docs.pazdzewicz.de/devops/2025/12/03/modern-system-design</id><content type="html" xml:base="https://docs.pazdzewicz.de/devops/2025/12/03/modern-system-design.html"><![CDATA[<p>Ich möchte hier anhand eines Beispiels einer einfachen URL Shortener erklären wie man eine moderne Systemarchitektur plant.</p>

<h2 id="inhaltsverzeichnis">Inhaltsverzeichnis</h2>

<ol>
  <li><a href="#überblick-was-soll-das-system-tun">Überblick: Was soll das System tun?</a></li>
  <li><a href="#anforderungen">Anforderungen</a></li>
  <li><a href="#problemstellung-warum-brauchen-wir-diese-architektur">Problemstellung: Warum brauchen wir diese Architektur?</a>
    <ul>
      <li>3.1 <a href="#problem-1-datenbank-performance-als-flaschenhals">Problem 1: Datenbank-Performance als Flaschenhals</a></li>
      <li>3.2 <a href="#problem-2-single-point-of-failure">Problem 2: Single Point of Failure</a></li>
      <li>3.3 <a href="#problem-3-skalierbarkeits-limits">Problem 3: Skalierbarkeits-Limits</a></li>
      <li>3.4 <a href="#problem-4-latenz-und-benutzererfahrung">Problem 4: Latenz und Benutzererfahrung</a></li>
    </ul>
  </li>
  <li><a href="#system-architektur">System-Architektur</a>
    <ul>
      <li>4.1 <a href="#komponenten-übersicht">Komponenten-Übersicht</a></li>
      <li>4.2 <a href="#1-datenbank-design">Datenbank-Design</a></li>
      <li>4.3 <a href="#2-short-code-generierung">Short-Code-Generierung</a></li>
      <li>4.4 <a href="#3-api-endpunkte">API-Endpunkte</a>
        <ul>
          <li>4.4.1 <a href="#post-apishorten">POST /api/shorten</a></li>
          <li>4.4.2 <a href="#get-short_code">GET /{short_code}</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#4-redis-caching-strategie-warum-und-wie">Redis-Caching-Strategie: Warum und wie?</a>
    <ul>
      <li>5.1 <a href="#warum-redis-das-performance-problem-verstehen">Warum Redis? Das Performance-Problem verstehen</a></li>
      <li>5.2 <a href="#was-macht-redis-genau">Was macht Redis genau?</a></li>
      <li>5.3 <a href="#cache-strategien-im-detail">Cache-Strategien im Detail</a></li>
      <li>5.4 <a href="#redis-konfiguration-und-memory-management">Redis-Konfiguration und Memory-Management</a></li>
      <li>5.5 <a href="#redis-cluster-für-hohe-verfügbarkeit">Redis-Cluster für hohe Verfügbarkeit</a></li>
    </ul>
  </li>
  <li><a href="#5-load-balancing-was-macht-ein-load-balancer-genau">Load Balancing: Was macht ein Load-Balancer genau?</a>
    <ul>
      <li>6.1 <a href="#das-problem-warum-brauchen-wir-einen-load-balancer">Das Problem: Warum brauchen wir einen Load-Balancer?</a></li>
      <li>6.2 <a href="#was-macht-ein-load-balancer-genau">Was macht ein Load-Balancer genau?</a></li>
      <li>6.3 <a href="#load-balancing-algorithmen">Load-Balancing-Algorithmen</a></li>
      <li>6.4 <a href="#health-checks-warum-sind-sie-kritisch">Health Checks: Warum sind sie kritisch?</a></li>
      <li>6.5 <a href="#session-persistenz-sticky-sessions">Session-Persistenz (Sticky Sessions)</a></li>
      <li>6.6 <a href="#ssltls-termination">SSL/TLS Termination</a></li>
      <li>6.7 <a href="#rate-limiting-und-ddos-schutz">Rate Limiting und DDoS-Schutz</a></li>
      <li>6.8 <a href="#traefik-konfiguration-vollständiges-beispiel">Traefik-Konfiguration: Vollständiges Beispiel</a></li>
      <li>6.9 <a href="#monitoring-und-metriken">Monitoring und Metriken</a></li>
    </ul>
  </li>
  <li><a href="#6-skalierbarkeit-warum-horizontale-skalierung">Skalierbarkeit: Warum horizontale Skalierung?</a>
    <ul>
      <li>7.1 <a href="#das-problem-vertikale-vs-horizontale-skalierung">Das Problem: Vertikale vs. Horizontale Skalierung</a></li>
      <li>7.2 <a href="#warum-horizontale-skalierung-für-unseren-url-shortener">Warum horizontale Skalierung für unseren URL-Shortener?</a></li>
      <li>7.3 <a href="#anforderungen-für-horizontale-skalierung">Anforderungen für horizontale Skalierung</a></li>
      <li>7.4 <a href="#skalierung-in-der-praxis">Skalierung in der Praxis</a></li>
      <li>7.5 <a href="#skalierungs-limits-und-bottlenecks">Skalierungs-Limits und Bottlenecks</a></li>
      <li>7.6 <a href="#skalierungsschritte-praktisches-beispiel">Skalierungsschritte: Praktisches Beispiel</a></li>
      <li>7.7 <a href="#monitoring-der-skalierung">Monitoring der Skalierung</a></li>
    </ul>
  </li>
  <li><a href="#7-erweiterte-optimierungen">Erweiterte Optimierungen</a></li>
  <li><a href="#zusammenfassung">Zusammenfassung</a></li>
</ol>

<hr />

<h2 id="überblick-was-soll-das-system-tun">Überblick: Was soll das System tun?</h2>

<p>Am Anfang überlegen wir uns wie wir generell die Short-URLs Designen. Dazu beginnen wir was eigentlich gemacht werden soll:</p>

<ol>
  <li>User gibt eine lange URL ein</li>
  <li>Die URL wird von einem API-Endpunkt entgegen genommen und gibt eine Short-URL zurück und speichert die Short-URL in einer Datenbank</li>
  <li>Der User ruft die Short-URL auf, es wird in einer Datenbank nachgeschaut ob die Short-URL vorhanden ist, holt die Value und schickt den Nutzer mit einem HTTP-301 weiter.</li>
</ol>

<h2 id="anforderungen">Anforderungen</h2>

<ul>
  <li>Der Short-Code sollte maximal 7 Zeichen lang sein. Mit einer Base64-Konvertierung haben wir kein Problem mit doppelten Indexen.</li>
  <li>Die Server sollten horizontal Skalierbar sein, dazu ist wichtig das ein Load-Balancer (am besten Traefik) die Last verteilt.</li>
  <li>Damit man keine doppelten Short-URLs erstellt sollte man einen Redis nutzen um die Datenbank zu cachen und so anfragen schneller umzusetzen.</li>
</ul>

<h2 id="problemstellung-warum-brauchen-wir-diese-architektur">Problemstellung: Warum brauchen wir diese Architektur?</h2>

<p>Bevor wir in die Details gehen, müssen wir verstehen, welche Probleme wir lösen müssen. Ein naives System mit nur einem Server und einer Datenbank würde bei steigender Last schnell an seine Grenzen stoßen.</p>

<h3 id="problem-1-datenbank-performance-als-flaschenhals">Problem 1: Datenbank-Performance als Flaschenhals</h3>

<p><strong>Das Problem:</strong>
Stellen Sie sich vor, Sie haben 10.000 Anfragen pro Sekunde für Redirects. Jede Anfrage muss:</p>
<ol>
  <li>Eine Datenbankverbindung aufbauen (5-10ms Overhead)</li>
  <li>Eine SQL-Query ausführen (10-50ms je nach Last)</li>
  <li>Das Ergebnis zurückgeben</li>
</ol>

<p>Bei 10.000 Anfragen/Sekunde bedeutet das:</p>
<ul>
  <li><strong>Ohne Cache:</strong> 10.000 × 50ms = 500 Sekunden Gesamtzeit → System bricht zusammen</li>
  <li><strong>Mit Cache (Redis):</strong> 9.500 × 0.1ms (Cache-Hit) + 500 × 50ms (Cache-Miss) = 25 Sekunden → System läuft stabil</li>
</ul>

<p><strong>Die Lösung:</strong> Redis als In-Memory-Cache reduziert Datenbankzugriffe um 95-99%.</p>

<h3 id="problem-2-single-point-of-failure">Problem 2: Single Point of Failure</h3>

<p><strong>Das Problem:</strong>
Ein einzelner Server kann:</p>
<ul>
  <li>Ausfallen (Hardware-Fehler, Software-Crash)</li>
  <li>Überlastet werden (CPU/Memory-Limits erreicht)</li>
  <li>Wartungsarbeiten erfordern (Updates, Patches)</li>
</ul>

<p>Wenn dieser eine Server ausfällt, ist die gesamte Anwendung offline.</p>

<p><strong>Die Lösung:</strong> Mehrere Server (horizontale Skalierung) + Load-Balancer sorgen für:</p>
<ul>
  <li><strong>Hochverfügbarkeit:</strong> Wenn ein Server ausfällt, übernehmen die anderen</li>
  <li><strong>Lastverteilung:</strong> Kein einzelner Server wird überlastet</li>
  <li><strong>Wartbarkeit:</strong> Server können einzeln aktualisiert werden</li>
</ul>

<h3 id="problem-3-skalierbarkeits-limits">Problem 3: Skalierbarkeits-Limits</h3>

<p><strong>Das Problem:</strong>
Ein einzelner Server hat physikalische Limits:</p>
<ul>
  <li>CPU: Maximal 32-64 Cores pro Server</li>
  <li>Memory: Maximal 512GB-1TB RAM</li>
  <li>Netzwerk: Maximal 10-100 Gbps</li>
</ul>

<p>Wenn Sie wachsen, müssen Sie entweder:</p>
<ul>
  <li><strong>Vertikal skalieren:</strong> Größere, teurere Server kaufen (sehr teuer, schnell an Limits)</li>
  <li><strong>Horizontal skalieren:</strong> Mehr günstige Server hinzufügen (kosteneffizient, praktisch unbegrenzt)</li>
</ul>

<p><strong>Die Lösung:</strong> Horizontale Skalierung mit Load-Balancer ermöglicht praktisch unbegrenztes Wachstum.</p>

<h3 id="problem-4-latenz-und-benutzererfahrung">Problem 4: Latenz und Benutzererfahrung</h3>

<p><strong>Das Problem:</strong>
Benutzer erwarten Antwortzeiten unter 100ms. Bei einer Datenbank-Query:</p>
<ul>
  <li>Lokale Datenbank: 5-20ms</li>
  <li>Remote-Datenbank: 20-100ms (abhängig von Netzwerk-Latenz)</li>
  <li>Bei hoher Last: 100-1000ms+ (Datenbank wird zum Flaschenhals)</li>
</ul>

<p><strong>Die Lösung:</strong> Redis liefert Antworten in &lt;1ms, was die Benutzererfahrung drastisch verbessert.</p>

<h2 id="system-architektur">System-Architektur</h2>

<blockquote>
  <p><strong>Wichtiger Hinweis:</strong> Alle Code-Beispiele in diesem Tutorial sind <strong>vereinfachte Meta-Code-Beispiele</strong> zur Veranschaulichung der Konzepte und Logik. Sie dienen dem Verständnis der System-Architektur und sind nicht als vollständige, produktionsreife Implementierungen gedacht. In einer produktiven Umgebung wären zusätzlich zu berücksichtigen: umfassende Fehlerbehandlung, Input-Validierung, Security-Best-Practices, Logging, Monitoring, Connection-Pooling, Migrationen, Tests, Dokumentation und viele weitere Aspekte.</p>
</blockquote>

<h3 id="komponenten-übersicht">Komponenten-Übersicht</h3>

<p>Unser System besteht aus folgenden Komponenten:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────┐
│   Client    │
└──────┬──────┘
       ▼
┌─────────────────┐
│  Load Balancer  │
└──────┬──────────┘
       ├──────────────┬──────────────┐
       ▼              ▼              ▼
┌──────────┐   ┌──────────┐   ┌──────────┐
│  API-1   │   │  API-2   │   │  API-3   │  (Horizontale Skalierung)
└────┬─────┘   └────┬─────┘   └────┬─────┘
     └──────┬───────┴──────┬───────┘
            ▼              ▼
      ┌─────────┐    ┌─────────┐
      │  Cache  │    │   DB    │  
      └─────────┘    └─────────┘
</code></pre></div></div>

<h3 id="1-datenbank-design">1. Datenbank-Design</h3>

<blockquote>
  <p><strong>Hinweis:</strong> Die folgenden Code-Beispiele sind vereinfachte Meta-Code-Beispiele zur Veranschaulichung der Konzepte. In einer produktiven Umgebung wären zusätzliche Aspekte wie Fehlerbehandlung, Validierung, Migrationen etc. zu berücksichtigen.</p>
</blockquote>

<p>Zuerst müssen wir die Datenbank-Struktur definieren. Wir benötigen eine Tabelle für die URL-Mappings:</p>

<p><strong>SQL Schema (Meta-Code Beispiel):</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Vereinfachtes Schema zur Veranschaulichung</span>
<span class="c1">-- In Produktion: Weitere Indizes, Constraints, Partitionierung etc.</span>

<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">url_mappings</span> <span class="p">(</span>
    <span class="n">id</span> <span class="n">BIGSERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>                    <span class="c1">-- Eindeutige ID</span>
    <span class="n">short_code</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span> <span class="k">UNIQUE</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>       <span class="c1">-- 7-stelliger Code</span>
    <span class="n">original_url</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>                  <span class="c1">-- Original-URL</span>
    <span class="n">created_at</span> <span class="nb">TIMESTAMP</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>
    <span class="n">access_count</span> <span class="nb">BIGINT</span> <span class="k">DEFAULT</span> <span class="mi">0</span><span class="p">,</span>                <span class="c1">-- Optional: Analytics</span>
    <span class="k">INDEX</span> <span class="n">idx_short_code</span> <span class="p">(</span><span class="n">short_code</span><span class="p">)</span>            <span class="c1">-- Index für schnelle Lookups</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>Wichtige Überlegungen:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">short_code</code> ist eindeutig und indiziert für schnelle Lookups</li>
  <li><code class="language-plaintext highlighter-rouge">original_url</code> speichert die vollständige URL</li>
  <li><code class="language-plaintext highlighter-rouge">access_count</code> für Analytics (optional)</li>
  <li>In Produktion: Weitere Optimierungen wie Partitionierung, Read-Replicas etc.</li>
</ul>

<h3 id="2-short-code-generierung">2. Short-Code-Generierung</h3>

<p>Der Short-Code wird aus einer eindeutigen ID generiert. Hier ist ein vereinfachtes Meta-Code-Beispiel:</p>

<p><strong>Meta-Code Beispiel (Python-ähnlich):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Vereinfachtes Beispiel zur Veranschaulichung
# In Produktion: Kollisionsprüfung, Retry-Logik etc.
</span>
<span class="kn">import</span> <span class="n">base64</span>
<span class="kn">import</span> <span class="n">hashlib</span>

<span class="k">def</span> <span class="nf">generate_short_code</span><span class="p">(</span><span class="n">url_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Generiert einen 7-stelligen Base64-kodierten Short-Code
    aus einer numerischen ID.
    
    Logik:
    1. Konvertiere ID zu Bytes
    2. Base64-kodiere
    3. Kürze auf 7 Zeichen
    </span><span class="sh">"""</span>
    <span class="n">id_bytes</span> <span class="o">=</span> <span class="n">url_id</span><span class="p">.</span><span class="nf">to_bytes</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="n">byteorder</span><span class="o">=</span><span class="sh">'</span><span class="s">big</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">encoded</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="nf">urlsafe_b64encode</span><span class="p">(</span><span class="n">id_bytes</span><span class="p">).</span><span class="nf">decode</span><span class="p">(</span><span class="sh">'</span><span class="s">utf-8</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">short_code</span> <span class="o">=</span> <span class="n">encoded</span><span class="p">[:</span><span class="mi">7</span><span class="p">].</span><span class="nf">rstrip</span><span class="p">(</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">short_code</span>

<span class="c1"># Alternative: Hash-basiert (für bessere Verteilung)
</span><span class="k">def</span> <span class="nf">generate_short_code_from_url</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">salt</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="sh">""</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Generiert einen Short-Code aus der URL selbst.
    Vorteil: Deterministisch (gleiche URL = gleicher Code)
    </span><span class="sh">"""</span>
    <span class="n">hash_input</span> <span class="o">=</span> <span class="p">(</span><span class="n">url</span> <span class="o">+</span> <span class="n">salt</span><span class="p">).</span><span class="nf">encode</span><span class="p">(</span><span class="sh">'</span><span class="s">utf-8</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">hash_bytes</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">hash_input</span><span class="p">).</span><span class="nf">digest</span><span class="p">()[:</span><span class="mi">5</span><span class="p">]</span>
    <span class="n">encoded</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="nf">urlsafe_b64encode</span><span class="p">(</span><span class="n">hash_bytes</span><span class="p">).</span><span class="nf">decode</span><span class="p">(</span><span class="sh">'</span><span class="s">utf-8</span><span class="sh">'</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">encoded</span><span class="p">[:</span><span class="mi">7</span><span class="p">].</span><span class="nf">rstrip</span><span class="p">(</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="3-api-endpunkte">3. API-Endpunkte</h3>

<blockquote>
  <p><strong>Hinweis:</strong> Die folgenden Code-Beispiele sind vereinfachte Meta-Code-Beispiele zur Veranschaulichung der Logik. In Produktion wären zusätzlich zu implementieren: Input-Validierung, Rate-Limiting, umfassende Fehlerbehandlung, Logging, Monitoring, Connection-Pooling etc.</p>
</blockquote>

<h4 id="post-apishorten">POST /api/shorten</h4>

<p><strong>API-Spezifikation:</strong></p>

<p><strong>Request:</strong></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://www.example.com/very/long/url/path"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Response:</strong></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"short_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aB3dEfG"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"short_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://short.ly/aB3dEfG"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"original_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://www.example.com/very/long/url/path"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Meta-Code Implementierung (vereinfacht):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Meta-Code: Vereinfachtes Beispiel zur Veranschaulichung der Logik
# Framework-agnostisch dargestellt
</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/api/shorten</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">shorten_url</span><span class="p">():</span>
    <span class="c1"># 1. Request-Daten extrahieren
</span>    <span class="n">original_url</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">get_json</span><span class="p">()[</span><span class="sh">'</span><span class="s">url</span><span class="sh">'</span><span class="p">]</span>
    
    <span class="c1"># 2. Cache-Check: Existiert bereits ein Short-Code für diese URL?
</span>    <span class="n">cache_key</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">url:</span><span class="si">{</span><span class="n">original_url</span><span class="si">}</span><span class="sh">"</span>
    <span class="n">cached_code</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="n">cached_code</span><span class="p">:</span>
        <span class="c1"># Cache-Hit: Gebe existierenden Code zurück
</span>        <span class="k">return</span> <span class="p">{</span>
            <span class="sh">'</span><span class="s">short_code</span><span class="sh">'</span><span class="p">:</span> <span class="n">cached_code</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">short_url</span><span class="sh">'</span><span class="p">:</span> <span class="sa">f</span><span class="sh">'</span><span class="s">https://short.ly/</span><span class="si">{</span><span class="n">cached_code</span><span class="si">}</span><span class="sh">'</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">original_url</span><span class="sh">'</span><span class="p">:</span> <span class="n">original_url</span>
        <span class="p">}</span>
    
    <span class="c1"># 3. Cache-Miss: Erstelle neuen Eintrag
</span>    <span class="c1"># 3a. Generiere Short-Code
</span>    <span class="n">url_id</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">get_next_id</span><span class="p">()</span>  <span class="c1"># Oder Auto-Increment
</span>    <span class="n">short_code</span> <span class="o">=</span> <span class="nf">generate_short_code</span><span class="p">(</span><span class="n">url_id</span><span class="p">)</span>
    
    <span class="c1"># 3b. Speichere in Datenbank
</span>    <span class="n">db</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="sh">'</span><span class="s">url_mappings</span><span class="sh">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="sh">'</span><span class="s">short_code</span><span class="sh">'</span><span class="p">:</span> <span class="n">short_code</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">original_url</span><span class="sh">'</span><span class="p">:</span> <span class="n">original_url</span>
    <span class="p">})</span>
    
    <span class="c1"># 3c. Cache speichern (beide Richtungen)
</span>    <span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">url:</span><span class="si">{</span><span class="n">original_url</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="mi">3600</span><span class="p">,</span> <span class="n">short_code</span><span class="p">)</span>
    <span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">code:</span><span class="si">{</span><span class="n">short_code</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="mi">86400</span><span class="p">,</span> <span class="n">original_url</span><span class="p">)</span>
    
    <span class="c1"># 4. Response zurückgeben
</span>    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">'</span><span class="s">short_code</span><span class="sh">'</span><span class="p">:</span> <span class="n">short_code</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">short_url</span><span class="sh">'</span><span class="p">:</span> <span class="sa">f</span><span class="sh">'</span><span class="s">https://short.ly/</span><span class="si">{</span><span class="n">short_code</span><span class="si">}</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">original_url</span><span class="sh">'</span><span class="p">:</span> <span class="n">original_url</span>
    <span class="p">}</span>
</code></pre></div></div>

<p><strong>Logik-Flow:</strong></p>
<ol>
  <li><strong>Cache-Check:</strong> Prüfe ob URL bereits existiert → verhindert Duplikate</li>
  <li><strong>Code-Generierung:</strong> Erstelle neuen eindeutigen Short-Code</li>
  <li><strong>Datenbank-Speicherung:</strong> Persistiere Mapping</li>
  <li><strong>Cache-Update:</strong> Speichere in beide Richtungen für schnelle Lookups</li>
</ol>

<h4 id="get-short_code">GET /{short_code}</h4>

<p><strong>API-Spezifikation:</strong></p>

<p><strong>Request:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /abc123
</code></pre></div></div>

<p><strong>Response:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP 301 Redirect
Location: https://www.example.com/very/long/url/path
</code></pre></div></div>

<p><strong>Meta-Code Implementierung (vereinfacht):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Meta-Code: Vereinfachtes Beispiel zur Veranschaulichung der Logik
</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/&lt;short_code&gt;</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">redirect_to_url</span><span class="p">(</span><span class="n">short_code</span><span class="p">):</span>
    <span class="c1"># 1. Cache-Check: Ist URL im Cache?
</span>    <span class="n">cache_key</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">code:</span><span class="si">{</span><span class="n">short_code</span><span class="si">}</span><span class="sh">"</span>
    <span class="n">cached_url</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="n">cached_url</span><span class="p">:</span>
        <span class="c1"># Cache-Hit: Sofortiger Redirect (sehr schnell: &lt;1ms)
</span>        <span class="k">return</span> <span class="nf">redirect</span><span class="p">(</span><span class="n">cached_url</span><span class="p">,</span> <span class="n">code</span><span class="o">=</span><span class="mi">301</span><span class="p">)</span>
    
    <span class="c1"># 2. Cache-Miss: Hole aus Datenbank
</span>    <span class="n">original_url</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span>
        <span class="sh">"</span><span class="s">SELECT original_url FROM url_mappings WHERE short_code = ?</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">short_code</span>
    <span class="p">)</span>
    
    <span class="k">if</span> <span class="ow">not</span> <span class="n">original_url</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">error</span><span class="p">(</span><span class="mi">404</span><span class="p">,</span> <span class="sh">'</span><span class="s">Short URL not found</span><span class="sh">'</span><span class="p">)</span>
    
    <span class="c1"># 3. Cache für zukünftige Anfragen speichern
</span>    <span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="n">cache_key</span><span class="p">,</span> <span class="mi">86400</span><span class="p">,</span> <span class="n">original_url</span><span class="p">)</span>  <span class="c1"># 24 Stunden
</span>    
    <span class="c1"># 4. Optional: Access-Count asynchron aktualisieren
</span>    <span class="c1"># (Nicht blockierend, läuft im Hintergrund)
</span>    <span class="nf">async_update_access_count</span><span class="p">(</span><span class="n">short_code</span><span class="p">)</span>
    
    <span class="c1"># 5. Redirect zurückgeben
</span>    <span class="k">return</span> <span class="nf">redirect</span><span class="p">(</span><span class="n">original_url</span><span class="p">,</span> <span class="n">code</span><span class="o">=</span><span class="mi">301</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Logik-Flow:</strong></p>
<ol>
  <li><strong>Cache-Check:</strong> Prüfe ob Short-Code im Cache → 95-99% der Fälle</li>
  <li><strong>Datenbank-Fallback:</strong> Falls nicht im Cache → Hole aus DB</li>
  <li><strong>Cache-Update:</strong> Speichere für zukünftige Anfragen</li>
  <li><strong>Redirect:</strong> HTTP 301 Permanent Redirect zur Original-URL</li>
</ol>

<h3 id="4-redis-caching-strategie-warum-und-wie">4. Redis-Caching-Strategie: Warum und wie?</h3>

<h4 id="warum-redis-das-performance-problem-verstehen">Warum Redis? Das Performance-Problem verstehen</h4>

<p><strong>Ohne Redis (nur Datenbank):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Anfrage → API-Server → Datenbank-Query (50ms) → Antwort
</code></pre></div></div>

<p>Bei 10.000 Anfragen/Sekunde:</p>
<ul>
  <li>Datenbank muss 10.000 Queries/Sekunde verarbeiten</li>
  <li>Jede Query benötigt 50ms → Datenbank wird überlastet</li>
  <li>System bricht zusammen oder wird extrem langsam</li>
</ul>

<p><strong>Mit Redis (Cache-Layer):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Anfrage → API-Server → Redis-Cache (0.1ms) → Antwort
         ↓ (Cache-Miss, 5% der Fälle)
         → Datenbank-Query (50ms) → Cache speichern → Antwort
</code></pre></div></div>

<p>Bei 10.000 Anfragen/Sekunde:</p>
<ul>
  <li>9.500 Anfragen aus Cache (0.1ms) = 0.95 Sekunden</li>
  <li>500 Anfragen aus Datenbank (50ms) = 25 Sekunden</li>
  <li><strong>Gesamt: ~26 Sekunden statt 500 Sekunden = 95% Performance-Gewinn</strong></li>
</ul>

<h4 id="was-macht-redis-genau">Was macht Redis genau?</h4>

<p>Redis ist ein <strong>In-Memory-Datenbank</strong> (alle Daten im RAM), was sie extrem schnell macht:</p>

<table>
  <thead>
    <tr>
      <th>Operation</th>
      <th>Datenbank (PostgreSQL)</th>
      <th>Redis</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Einfacher Read</td>
      <td>5-50ms</td>
      <td>0.1-1ms</td>
    </tr>
    <tr>
      <td>Einfacher Write</td>
      <td>10-100ms</td>
      <td>0.1-1ms</td>
    </tr>
    <tr>
      <td>Durchsatz</td>
      <td>1.000-10.000 Ops/s</td>
      <td>100.000-1.000.000 Ops/s</td>
    </tr>
  </tbody>
</table>

<p><strong>Warum ist Redis so schnell?</strong></p>
<ol>
  <li><strong>Keine Festplatten-I/O:</strong> Alles im RAM (1000x schneller als SSD)</li>
  <li><strong>Einfache Datenstrukturen:</strong> Key-Value-Store ohne komplexe Joins</li>
  <li><strong>Single-threaded:</strong> Keine Locks, keine Race-Conditions</li>
  <li><strong>Optimiert für Geschwindigkeit:</strong> C-Implementierung, minimaler Overhead</li>
</ol>

<h4 id="cache-strategien-im-detail">Cache-Strategien im Detail</h4>

<p>Redis wird für zwei kritische Zwecke verwendet:</p>

<p><strong>1. URL → Short-Code Cache (Verhindert Duplikate)</strong></p>

<p><strong>Problem ohne Cache:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># User sendet gleiche URL zweimal
</span><span class="n">POST</span> <span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">shorten</span> <span class="p">{</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">https://example.com</span><span class="sh">"</span><span class="p">}</span>
<span class="err">→</span> <span class="n">Datenbank</span><span class="o">-</span><span class="n">Query</span><span class="p">:</span> <span class="sh">"</span><span class="s">SELECT * WHERE url = ...</span><span class="sh">"</span> <span class="p">(</span><span class="mi">50</span><span class="n">ms</span><span class="p">)</span>
<span class="err">→</span> <span class="n">Nicht</span> <span class="n">gefunden</span> <span class="err">→</span> <span class="n">Neuer</span> <span class="n">Eintrag</span> <span class="n">erstellt</span>

<span class="n">POST</span> <span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">shorten</span> <span class="p">{</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">https://example.com</span><span class="sh">"</span><span class="p">}</span>  <span class="c1"># Gleiche URL!
</span><span class="err">→</span> <span class="n">Datenbank</span><span class="o">-</span><span class="n">Query</span><span class="p">:</span> <span class="sh">"</span><span class="s">SELECT * WHERE url = ...</span><span class="sh">"</span> <span class="p">(</span><span class="mi">50</span><span class="n">ms</span><span class="p">)</span>
<span class="err">→</span> <span class="n">Nicht</span> <span class="n">gefunden</span> <span class="err">→</span> <span class="n">Noch</span> <span class="n">ein</span> <span class="n">Eintrag</span> <span class="nf">erstellt </span><span class="p">(</span><span class="n">DUPLIKAT</span><span class="err">!</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Lösung mit Cache:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Erste Anfrage
</span><span class="n">POST</span> <span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">shorten</span> <span class="p">{</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">https://example.com</span><span class="sh">"</span><span class="p">}</span>
<span class="err">→</span> <span class="n">Redis</span><span class="o">-</span><span class="n">Check</span><span class="p">:</span> <span class="sh">"</span><span class="s">GET url:https://example.com</span><span class="sh">"</span> <span class="p">(</span><span class="mf">0.1</span><span class="n">ms</span><span class="p">)</span>
<span class="err">→</span> <span class="n">Nicht</span> <span class="n">gefunden</span> <span class="err">→</span> <span class="n">Datenbank</span> <span class="err">→</span> <span class="n">Cache</span> <span class="n">speichern</span>
<span class="err">→</span> <span class="n">Redis</span><span class="p">:</span> <span class="sh">"</span><span class="s">SETEX url:https://example.com 3600 abc123</span><span class="sh">"</span>

<span class="c1"># Zweite Anfrage (gleiche URL)
</span><span class="n">POST</span> <span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">shorten</span> <span class="p">{</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">https://example.com</span><span class="sh">"</span><span class="p">}</span>
<span class="err">→</span> <span class="n">Redis</span><span class="o">-</span><span class="n">Check</span><span class="p">:</span> <span class="sh">"</span><span class="s">GET url:https://example.com</span><span class="sh">"</span> <span class="p">(</span><span class="mf">0.1</span><span class="n">ms</span><span class="p">)</span>
<span class="err">→</span> <span class="n">Gefunden</span><span class="err">!</span> <span class="err">→</span> <span class="n">Sofort</span> <span class="n">zurückgeben</span> <span class="p">(</span><span class="n">keine</span> <span class="n">Datenbank</span><span class="o">-</span><span class="n">Query</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>2. Short-Code → URL Cache (Beschleunigt Redirects)</strong></p>

<p><strong>Das Problem:</strong>
Redirects sind der häufigste Operationstyp (99% aller Anfragen). Jeder Redirect ohne Cache bedeutet:</p>
<ul>
  <li>Datenbankverbindung aufbauen</li>
  <li>SQL-Query ausführen</li>
  <li>Ergebnis zurückgeben</li>
</ul>

<p>Bei 1 Million Redirects/Tag = 11.5 Redirects/Sekunde → Datenbank wird überlastet.</p>

<p><strong>Die Lösung:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Redirect-Anfrage
</span><span class="n">GET</span> <span class="o">/</span><span class="n">abc123</span>
<span class="err">→</span> <span class="n">Redis</span><span class="p">:</span> <span class="sh">"</span><span class="s">GET code:abc123</span><span class="sh">"</span> <span class="p">(</span><span class="mf">0.1</span><span class="n">ms</span><span class="p">)</span>
<span class="err">→</span> <span class="n">Gefunden</span><span class="err">!</span> <span class="err">→</span> <span class="n">Sofortiger</span> <span class="nc">Redirect </span><span class="p">(</span><span class="n">keine</span> <span class="n">Datenbank</span><span class="o">-</span><span class="n">Query</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Cache-Hit-Rate Optimierung:</strong></p>

<p>Eine gute Cache-Hit-Rate liegt bei 95-99%. Das bedeutet:</p>
<ul>
  <li>95-99% der Anfragen werden aus Cache bedient (&lt;1ms)</li>
  <li>1-5% der Anfragen gehen zur Datenbank (50ms)</li>
</ul>

<p><strong>Cache-Strategie im Code (Meta-Code Beispiel):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Meta-Code: Vereinfachtes Beispiel zur Veranschaulichung
# Cache-Aside Pattern: Cache wird "neben" der Datenbank verwendet
</span>
<span class="c1"># Beim Erstellen einer Short-URL
# Cache in beide Richtungen für schnelle Lookups
</span><span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">url:</span><span class="si">{</span><span class="n">original_url</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="mi">3600</span><span class="p">,</span> <span class="n">short_code</span><span class="p">)</span>      <span class="c1"># URL → Code (1 Stunde)
</span><span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">code:</span><span class="si">{</span><span class="n">short_code</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="mi">86400</span><span class="p">,</span> <span class="n">original_url</span><span class="p">)</span>    <span class="c1"># Code → URL (24 Stunden)
</span>
<span class="c1"># Beim Abrufen (Cache-Aside Pattern)
</span><span class="k">def</span> <span class="nf">get_url_from_cache_or_db</span><span class="p">(</span><span class="n">short_code</span><span class="p">):</span>
    <span class="c1"># 1. Prüfe Cache zuerst (schnell: &lt;1ms)
</span>    <span class="n">cached_url</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">code:</span><span class="si">{</span><span class="n">short_code</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">cached_url</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">cached_url</span>  <span class="c1"># Cache-Hit: Sofort zurückgeben
</span>    
    <span class="c1"># 2. Cache-Miss: Hole aus Datenbank (langsam: 50ms)
</span>    <span class="n">url</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="sh">"</span><span class="s">SELECT original_url WHERE short_code = ?</span><span class="sh">"</span><span class="p">,</span> <span class="n">short_code</span><span class="p">)</span>
    
    <span class="c1"># 3. Speichere im Cache für zukünftige Anfragen
</span>    <span class="k">if</span> <span class="n">url</span><span class="p">:</span>
        <span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">code:</span><span class="si">{</span><span class="n">short_code</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="mi">86400</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">url</span>
</code></pre></div></div>

<h4 id="redis-konfiguration-und-memory-management">Redis-Konfiguration und Memory-Management</h4>

<p><strong>Warum Memory-Limits wichtig sind:</strong></p>

<p>Redis speichert alles im RAM. Ohne Limits würde Redis:</p>
<ul>
  <li>Den gesamten verfügbaren RAM verbrauchen</li>
  <li>Das System zum Absturz bringen</li>
  <li>Andere Anwendungen verdrängen</li>
</ul>

<p><strong>Redis-Konfiguration (redis.conf):</strong></p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Maximal 2GB RAM für Redis
</span><span class="n">maxmemory</span> <span class="m">2</span><span class="n">gb</span>

<span class="c"># Eviction-Policy: Welche Keys werden gelöscht wenn Memory voll ist?
# allkeys-lru: Löscht am wenigsten genutzte Keys (LRU = Least Recently Used)
</span><span class="n">maxmemory</span>-<span class="n">policy</span> <span class="n">allkeys</span>-<span class="n">lru</span>

<span class="c"># Alternative Policies:
# - noeviction: Keine Keys löschen (Fehler wenn voll)
# - allkeys-lfu: Löscht am wenigsten häufig genutzte Keys
# - volatile-lru: Löscht nur Keys mit TTL
</span></code></pre></div></div>

<p><strong>TTL (Time-To-Live) Strategie (Meta-Code Beispiel):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Meta-Code: Beispiel für unterschiedliche TTL-Strategien
# TTL bestimmt, wie lange Daten im Cache bleiben
</span>
<span class="c1"># Kurze TTL für selten genutzte/temporäre Daten
</span><span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sh">"</span><span class="s">temp:session:123</span><span class="sh">"</span><span class="p">,</span> <span class="mi">300</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>              <span class="c1"># 5 Minuten
</span>
<span class="c1"># Mittlere TTL für normale Daten
</span><span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sh">"</span><span class="s">url:https://example.com</span><span class="sh">"</span><span class="p">,</span> <span class="mi">3600</span><span class="p">,</span> <span class="n">code</span><span class="p">)</span>       <span class="c1"># 1 Stunde
</span>
<span class="c1"># Lange TTL für häufig genutzte, stabile Daten
</span><span class="n">redis</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sh">"</span><span class="s">code:abc123</span><span class="sh">"</span><span class="p">,</span> <span class="mi">86400</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span>                   <span class="c1"># 24 Stunden
</span></code></pre></div></div>

<p><strong>Warum unterschiedliche TTLs?</strong></p>
<ul>
  <li><strong>Kurze TTL:</strong> Daten ändern sich häufig oder sind temporär</li>
  <li><strong>Lange TTL:</strong> Daten ändern sich selten, hohe Cache-Hit-Rate gewünscht</li>
  <li><strong>Strategie:</strong> Balance zwischen Cache-Hit-Rate und Datenkonsistenz</li>
</ul>

<h4 id="redis-cluster-für-hohe-verfügbarkeit">Redis-Cluster für hohe Verfügbarkeit</h4>

<p><strong>Problem: Single Redis-Instanz</strong></p>

<p>Wenn Redis ausfällt:</p>
<ul>
  <li>Alle Cache-Daten sind weg</li>
  <li>System fällt auf Datenbank zurück (langsam)</li>
  <li>Bei hoher Last: System kann zusammenbrechen</li>
</ul>

<p><strong>Lösung: Redis-Cluster oder Redis-Sentinel</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────┐    ┌─────────┐    ┌─────────┐
│ Redis-1 │    │ Redis-2 │    │ Redis-3 │
│ (Master)│◄───┤(Replica)│◄───┤(Replica)│
└─────────┘    └─────────┘    └─────────┘
     │              │              │
     └──────────────┴──────────────┘
                    │
              ┌─────────┐
              │ Sentinel│  (Überwacht Master, failover bei Ausfall)
              └─────────┘
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li><strong>Hochverfügbarkeit:</strong> Wenn Master ausfällt, übernimmt Replica</li>
  <li><strong>Read-Skalierung:</strong> Replicas können für Reads genutzt werden</li>
  <li><strong>Datenredundanz:</strong> Daten sind auf mehreren Servern gespeichert</li>
</ul>

<h3 id="5-load-balancing-was-macht-ein-load-balancer-genau">5. Load Balancing: Was macht ein Load-Balancer genau?</h3>

<h4 id="das-problem-warum-brauchen-wir-einen-load-balancer">Das Problem: Warum brauchen wir einen Load-Balancer?</h4>

<p><strong>Ohne Load-Balancer (ein Server):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client → API-Server (10.000 Anfragen/Sekunde)
</code></pre></div></div>

<p><strong>Probleme:</strong></p>
<ol>
  <li><strong>Single Point of Failure:</strong> Wenn Server ausfällt, ist alles offline</li>
  <li><strong>Performance-Limit:</strong> Ein Server kann nur begrenzte Anfragen verarbeiten</li>
  <li><strong>Wartungsprobleme:</strong> Server muss offline für Updates</li>
  <li><strong>Ressourcen-Limit:</strong> CPU/Memory-Limits werden erreicht</li>
</ol>

<p><strong>Mit Load-Balancer (mehrere Server):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client → Load-Balancer → API-Server-1 (3.333 Anfragen/Sekunde)
                       → API-Server-2 (3.333 Anfragen/Sekunde)
                       → API-Server-3 (3.333 Anfragen/Sekunde)
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ol>
  <li><strong>Hochverfügbarkeit:</strong> Wenn ein Server ausfällt, übernehmen die anderen</li>
  <li><strong>Skalierbarkeit:</strong> Mehr Server = mehr Kapazität</li>
  <li><strong>Wartbarkeit:</strong> Server können einzeln aktualisiert werden</li>
  <li><strong>Lastverteilung:</strong> Kein Server wird überlastet</li>
</ol>

<h4 id="was-macht-ein-load-balancer-genau">Was macht ein Load-Balancer genau?</h4>

<p>Ein Load-Balancer ist ein <strong>Reverse-Proxy</strong>, der zwischen Clients und Servern sitzt und Anfragen intelligent verteilt.</p>

<p><strong>Funktionsweise im Detail:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Client sendet Anfrage an Load-Balancer
   GET https://short.ly/abc123

2. Load-Balancer analysiert Anfrage
   - Welche Server sind verfügbar?
   - Welcher Server hat am wenigsten Last?
   - Welcher Algorithmus soll verwendet werden?

3. Load-Balancer wählt Server aus
   → API-Server-2 (geringste Last)

4. Load-Balancer leitet Anfrage weiter
   GET http://api-2:5000/abc123

5. Server verarbeitet Anfrage
   → Redis-Check → Redirect

6. Antwort geht zurück durch Load-Balancer
   HTTP 301 → Client
</code></pre></div></div>

<h4 id="load-balancing-algorithmen">Load-Balancing-Algorithmen</h4>

<p><strong>1. Round-Robin (Standard)</strong></p>

<p><strong>Wie es funktioniert:</strong></p>
<ul>
  <li>Anfragen werden sequenziell an Server verteilt</li>
  <li>Server 1 → Server 2 → Server 3 → Server 1 → …</li>
</ul>

<p><strong>Beispiel:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Anfrage 1 → Server 1
Anfrage 2 → Server 2
Anfrage 3 → Server 3
Anfrage 4 → Server 1
Anfrage 5 → Server 2
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li>Einfach zu implementieren</li>
  <li>Gleichmäßige Verteilung bei ähnlicher Server-Performance</li>
</ul>

<p><strong>Nachteile:</strong></p>
<ul>
  <li>Ignoriert aktuelle Server-Last</li>
  <li>Ignoriert Server-Kapazität (starker vs. schwacher Server)</li>
</ul>

<p><strong>2. Least Connections</strong></p>

<p><strong>Wie es funktioniert:</strong></p>
<ul>
  <li>Sendet Anfrage an Server mit den wenigsten aktiven Verbindungen</li>
</ul>

<p><strong>Beispiel:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server 1: 100 aktive Verbindungen
Server 2: 50 aktive Verbindungen  ← Wird gewählt
Server 3: 75 aktive Verbindungen
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li>Berücksichtigt aktuelle Server-Last</li>
  <li>Gut für langlebige Verbindungen (WebSockets, Streaming)</li>
</ul>

<p><strong>3. Weighted Round-Robin</strong></p>

<p><strong>Wie es funktioniert:</strong></p>
<ul>
  <li>Jeder Server hat ein Gewicht (z.B. Server 1 = 3, Server 2 = 1)</li>
  <li>Stärkere Server bekommen mehr Anfragen</li>
</ul>

<p><strong>Beispiel:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server 1 (Gewicht 3): 3 Anfragen
Server 2 (Gewicht 1): 1 Anfrage
Server 1: 3 Anfragen
Server 2: 1 Anfrage
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li>Berücksichtigt unterschiedliche Server-Kapazitäten</li>
  <li>Optimal wenn Server unterschiedlich stark sind</li>
</ul>

<p><strong>4. IP Hash (Sticky Sessions)</strong></p>

<p><strong>Wie es funktioniert:</strong></p>
<ul>
  <li>Hash der Client-IP bestimmt, welcher Server verwendet wird</li>
  <li>Gleiche IP → immer gleicher Server</li>
</ul>

<p><strong>Beispiel:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client 192.168.1.1 → Hash → Server 2 (immer)
Client 192.168.1.2 → Hash → Server 1 (immer)
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li>Session-Persistenz (wichtig für Stateful-Anwendungen)</li>
  <li>Cache-Freundlich (gleicher Client → gleicher Server)</li>
</ul>

<p><strong>Nachteile:</strong></p>
<ul>
  <li>Ungleichmäßige Verteilung bei wenigen Clients</li>
  <li>Problem wenn Server ausfällt (Sessions gehen verloren)</li>
</ul>

<h4 id="health-checks-warum-sind-sie-kritisch">Health Checks: Warum sind sie kritisch?</h4>

<p><strong>Das Problem ohne Health Checks:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server 2 ist abgestürzt, aber Load-Balancer weiß es nicht
→ Anfragen werden weiterhin an Server 2 gesendet
→ Client erhält Fehler (Timeout, 500 Error)
→ 33% der Anfragen schlagen fehl
</code></pre></div></div>

<p><strong>Die Lösung: Health Checks</strong></p>

<p>Ein Load-Balancer überprüft regelmäßig, ob Server erreichbar sind:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Traefik Health Check Konfiguration</span>
<span class="na">healthcheck</span><span class="pi">:</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s">/health</span>
  <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>      <span class="c1"># Alle 10 Sekunden prüfen</span>
  <span class="na">timeout</span><span class="pi">:</span> <span class="s">3s</span>       <span class="c1"># Timeout nach 3 Sekunden</span>
  <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>        <span class="c1"># 3 Fehlversuche = Server als "down" markieren</span>
</code></pre></div></div>

<p><strong>Health Check Endpoint im API-Server (Meta-Code Beispiel):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Meta-Code: Vereinfachtes Beispiel zur Veranschaulichung
# In Produktion: Timeouts, detaillierte Checks, Metriken etc.
</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/health</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">health_check</span><span class="p">():</span>
    <span class="c1"># Prüfe kritische Komponenten
</span>    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># 1. Redis erreichbar?
</span>        <span class="n">redis</span><span class="p">.</span><span class="nf">ping</span><span class="p">()</span>
        
        <span class="c1"># 2. Datenbank erreichbar?
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">ping</span><span class="p">()</span>  <span class="c1"># Oder einfache Query
</span>        
        <span class="c1"># 3. Optional: Weitere Checks
</span>        <span class="c1"># - Disk Space
</span>        <span class="c1"># - Memory Usage
</span>        <span class="c1"># - External Services
</span>        
        <span class="k">return</span> <span class="p">{</span><span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">healthy</span><span class="sh">'</span><span class="p">},</span> <span class="mi">200</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="c1"># Fehler: Server als "unhealthy" markieren
</span>        <span class="k">return</span> <span class="p">{</span><span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">unhealthy</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">error</span><span class="sh">'</span><span class="p">:</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">)},</span> <span class="mi">503</span>
</code></pre></div></div>

<p><strong>Health Check Workflow:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Load-Balancer sendet GET /health an Server
2. Server prüft: Redis OK? Datenbank OK?
3. Server antwortet: 200 OK oder 503 Unhealthy
4. Load-Balancer markiert Server als "up" oder "down"
5. Nur "up" Server erhalten Anfragen
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li><strong>Automatisches Failover:</strong> Abgestürzte Server werden automatisch ausgeschlossen</li>
  <li><strong>Selbstheilung:</strong> Wenn Server wieder online ist, wird er automatisch wieder eingebunden</li>
  <li><strong>Keine manuelle Intervention:</strong> System läuft weiter ohne Administrator</li>
</ul>

<h4 id="session-persistenz-sticky-sessions">Session-Persistenz (Sticky Sessions)</h4>

<p><strong>Das Problem:</strong></p>

<p>Bei Stateful-Anwendungen (z.B. Shopping-Cart):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request 1: Client → Load-Balancer → Server 1 (Cart wird erstellt)
Request 2: Client → Load-Balancer → Server 2 (Cart ist leer! Problem!)
</code></pre></div></div>

<p><strong>Die Lösung: Sticky Sessions</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Traefik Cookie-basierte Session-Persistenz</span>
<span class="na">labels</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.sticky.cookie=true"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.sticky.cookie.name=server_id"</span>
</code></pre></div></div>

<p><strong>Wie es funktioniert:</strong></p>
<ol>
  <li>Erste Anfrage → Load-Balancer wählt Server 1</li>
  <li>Load-Balancer setzt Cookie: <code class="language-plaintext highlighter-rouge">server_id=server1</code></li>
  <li>Weitere Anfragen → Cookie wird gelesen → Immer Server 1</li>
</ol>

<p><strong>Für unser URL-Shortener:</strong></p>
<ul>
  <li><strong>Nicht notwendig:</strong> Unsere API ist stateless (kein Session-State)</li>
  <li><strong>Aber nützlich:</strong> Kann Cache-Hit-Rate verbessern (gleicher Client → gleicher Server)</li>
</ul>

<h4 id="ssltls-termination">SSL/TLS Termination</h4>

<p><strong>Was ist SSL/TLS Termination?</strong></p>

<p>Der Load-Balancer übernimmt die SSL-Entschlüsselung:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client → [HTTPS verschlüsselt] → Load-Balancer → [HTTP unverschlüsselt] → API-Server
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li><strong>Performance:</strong> CPU-intensive SSL-Entschlüsselung nur im Load-Balancer</li>
  <li><strong>Zentrales Zertifikats-Management:</strong> Nur Load-Balancer braucht Zertifikate</li>
  <li><strong>Einfacheres Backend:</strong> API-Server müssen kein SSL handhaben</li>
</ul>

<p><strong>Traefik SSL-Konfiguration:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">traefik</span><span class="pi">:</span>
  <span class="na">command</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.letsencrypt.acme.email=admin@short.ly"</span>
    <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"</span>
    <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">./letsencrypt:/letsencrypt</span>
</code></pre></div></div>

<h4 id="rate-limiting-und-ddos-schutz">Rate Limiting und DDoS-Schutz</h4>

<p><strong>Das Problem: DDoS-Angriffe</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Angreifer sendet 100.000 Anfragen/Sekunde
→ Ohne Rate Limiting: Alle Server überlastet
→ System bricht zusammen
</code></pre></div></div>

<p><strong>Die Lösung: Rate Limiting im Load-Balancer</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Traefik Rate Limiting</span>
<span class="na">labels</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.middlewares.ratelimit.ratelimit.average=100"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.middlewares.ratelimit.ratelimit.period=1s"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.middlewares=ratelimit"</span>
</code></pre></div></div>

<p><strong>Wie es funktioniert:</strong></p>
<ul>
  <li>Maximal 100 Anfragen pro Sekunde pro Client</li>
  <li>Überschreitungen werden abgelehnt (429 Too Many Requests)</li>
  <li>Schützt Backend-Server vor Überlastung</li>
</ul>

<h4 id="traefik-konfiguration-vollständiges-beispiel">Traefik-Konfiguration: Vollständiges Beispiel</h4>

<p><strong>docker-compose.yml für Traefik:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.8'</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik:v2.10</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="c1"># API Dashboard (nur für Entwicklung)</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--api.insecure=true"</span>
      
      <span class="c1"># Docker Provider (automatische Service-Erkennung)</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.exposedbydefault=false"</span>
      
      <span class="c1"># Entry Points (Ports)</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.web.address=:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.websecure.address=:443"</span>
      
      <span class="c1"># SSL/TLS (Let's Encrypt)</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.letsencrypt.acme.email=admin@short.ly"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"</span>
      
      <span class="c1"># Logging</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--accesslog=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--log.level=INFO"</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">80:80"</span>      <span class="c1"># HTTP</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">443:443"</span>    <span class="c1"># HTTPS</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8080:8080"</span>  <span class="c1"># Traefik Dashboard</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="pi">-</span> <span class="s">./letsencrypt:/letsencrypt</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">app-network</span>

  <span class="na">api-1</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">./api</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="c1"># Traefik aktivieren</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      
      <span class="c1"># Routing-Regeln</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host(`short.ly`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.entrypoints=web,websecure"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.tls.certresolver=letsencrypt"</span>
      
      <span class="c1"># Service-Konfiguration</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.server.port=5000"</span>
      
      <span class="c1"># Load-Balancing-Algorithmus (Round-Robin ist Standard)</span>
      <span class="c1"># Für Least Connections:</span>
      <span class="c1"># - "traefik.http.services.api.loadbalancer.server.weight=1"</span>
      
      <span class="c1"># Health Checks</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.healthcheck.path=/health"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.healthcheck.interval=10s"</span>
      
      <span class="c1"># Rate Limiting (optional)</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.middlewares.ratelimit.ratelimit.average=100"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.middlewares=ratelimit"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">app-network</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">REDIS_HOST=redis</span>
      <span class="pi">-</span> <span class="s">DB_HOST=postgres</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">curl"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-f"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">http://localhost:5000/health"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">3s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>

  <span class="na">api-2</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">./api</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host(`short.ly`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.entrypoints=web,websecure"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.tls.certresolver=letsencrypt"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.server.port=5000"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.healthcheck.path=/health"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.healthcheck.interval=10s"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">app-network</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">REDIS_HOST=redis</span>
      <span class="pi">-</span> <span class="s">DB_HOST=postgres</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">curl"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-f"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">http://localhost:5000/health"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">3s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>

  <span class="na">api-3</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">./api</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host(`short.ly`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.entrypoints=web,websecure"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.tls.certresolver=letsencrypt"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.server.port=5000"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.healthcheck.path=/health"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.healthcheck.interval=10s"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">app-network</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">REDIS_HOST=redis</span>
      <span class="pi">-</span> <span class="s">DB_HOST=postgres</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">curl"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-f"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">http://localhost:5000/health"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">3s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>

  <span class="na">redis</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">redis:7-alpine</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">redis-data:/data</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">app-network</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru</span>

  <span class="na">postgres</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:15-alpine</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">POSTGRES_DB=urlshortener</span>
      <span class="pi">-</span> <span class="s">POSTGRES_USER=user</span>
      <span class="pi">-</span> <span class="s">POSTGRES_PASSWORD=password</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">postgres-data:/var/lib/postgresql/data</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">app-network</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">redis-data</span><span class="pi">:</span>
  <span class="na">postgres-data</span><span class="pi">:</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">app-network</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">bridge</span>
</code></pre></div></div>

<h4 id="monitoring-und-metriken">Monitoring und Metriken</h4>

<p><strong>Wichtige Load-Balancer-Metriken:</strong></p>

<ol>
  <li><strong>Request-Rate:</strong> Anfragen pro Sekunde</li>
  <li><strong>Response-Time:</strong> Durchschnittliche Antwortzeit</li>
  <li><strong>Error-Rate:</strong> Prozent der fehlgeschlagenen Anfragen</li>
  <li><strong>Server-Status:</strong> Welche Server sind “up” oder “down”?</li>
  <li><strong>Lastverteilung:</strong> Wie viele Anfragen pro Server?</li>
</ol>

<p><strong>Traefik Dashboard:</strong></p>

<p>Zugriff auf <code class="language-plaintext highlighter-rouge">http://localhost:8080</code> zeigt:</p>
<ul>
  <li>Alle konfigurierten Routen</li>
  <li>Status aller Backend-Server</li>
  <li>Request-Metriken in Echtzeit</li>
  <li>Health-Check-Status</li>
</ul>

<h3 id="6-skalierbarkeit-warum-horizontale-skalierung">6. Skalierbarkeit: Warum horizontale Skalierung?</h3>

<h4 id="das-problem-vertikale-vs-horizontale-skalierung">Das Problem: Vertikale vs. Horizontale Skalierung</h4>

<p><strong>Vertikale Skalierung (Scale-Up):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ein Server: 4 Cores, 8GB RAM
     ↓
Größerer Server: 8 Cores, 16GB RAM
     ↓
Noch größerer Server: 16 Cores, 32GB RAM
</code></pre></div></div>

<p><strong>Probleme:</strong></p>
<ol>
  <li><strong>Teuer:</strong> Größere Server kosten exponentiell mehr</li>
  <li><strong>Limits:</strong> Irgendwann gibt es keine größeren Server mehr</li>
  <li><strong>Downtime:</strong> Server muss offline für Hardware-Upgrade</li>
  <li><strong>Single Point of Failure:</strong> Ein Server = ein Ausfallpunkt</li>
</ol>

<p><strong>Horizontale Skalierung (Scale-Out):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 Server: 4 Cores, 8GB RAM
     ↓
3 Server: 12 Cores, 24GB RAM (gesamt)
     ↓
10 Server: 40 Cores, 80GB RAM (gesamt)
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ol>
  <li><strong>Kosteneffizient:</strong> Mehr kleine Server sind günstiger als ein großer</li>
  <li><strong>Praktisch unbegrenzt:</strong> Kann beliebig viele Server hinzufügen</li>
  <li><strong>Kein Downtime:</strong> Neue Server können live hinzugefügt werden</li>
  <li><strong>Hochverfügbarkeit:</strong> Wenn ein Server ausfällt, übernehmen andere</li>
</ol>

<h4 id="warum-horizontale-skalierung-für-unseren-url-shortener">Warum horizontale Skalierung für unseren URL-Shortener?</h4>

<p><strong>Szenario: Wachstum der Anwendung</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start: 100 Anfragen/Sekunde → 1 Server reicht
     ↓
Nach 6 Monaten: 1.000 Anfragen/Sekunde → 3 Server nötig
     ↓
Nach 1 Jahr: 10.000 Anfragen/Sekunde → 10 Server nötig
     ↓
Nach 2 Jahren: 100.000 Anfragen/Sekunde → 50 Server nötig
</code></pre></div></div>

<p><strong>Mit vertikaler Skalierung:</strong></p>
<ul>
  <li>Müssten Sie Server ständig ersetzen (teuer, Downtime)</li>
  <li>Irgendwann gibt es keine größeren Server mehr</li>
</ul>

<p><strong>Mit horizontaler Skalierung:</strong></p>
<ul>
  <li>Einfach mehr Server hinzufügen (günstig, kein Downtime)</li>
  <li>Praktisch unbegrenzt skalierbar</li>
</ul>

<h4 id="anforderungen-für-horizontale-skalierung">Anforderungen für horizontale Skalierung</h4>

<p><strong>1. Stateless API-Server</strong></p>

<p><strong>Was bedeutet “stateless”?</strong></p>

<p>Ein stateless Server speichert <strong>keinen lokalen State</strong> (keine Session-Daten, kein lokaler Cache).</p>

<p><strong>Stateless (gut für Skalierung):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Jede Anfrage ist unabhängig
</span><span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/&lt;short_code&gt;</span><span class="sh">'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">redirect</span><span class="p">(</span><span class="n">short_code</span><span class="p">):</span>
    <span class="c1"># Keine lokalen Variablen, keine Session-Daten
</span>    <span class="n">url</span> <span class="o">=</span> <span class="nf">get_url_from_redis_or_db</span><span class="p">(</span><span class="n">short_code</span><span class="p">)</span>  <span class="c1"># Externe Datenquelle
</span>    <span class="k">return</span> <span class="nf">redirect</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Stateful (schlecht für Skalierung):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Server speichert State lokal
</span><span class="n">session_data</span> <span class="o">=</span> <span class="p">{}</span>  <span class="c1"># Lokaler Speicher
</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/&lt;short_code&gt;</span><span class="sh">'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">redirect</span><span class="p">(</span><span class="n">short_code</span><span class="p">):</span>
    <span class="c1"># Problem: Wenn Request an anderen Server geht, ist State weg!
</span>    <span class="k">if</span> <span class="n">short_code</span> <span class="ow">in</span> <span class="n">session_data</span><span class="p">:</span>  <span class="c1"># Nur auf diesem Server!
</span>        <span class="k">return</span> <span class="nf">redirect</span><span class="p">(</span><span class="n">session_data</span><span class="p">[</span><span class="n">short_code</span><span class="p">])</span>
</code></pre></div></div>

<p><strong>Warum ist Stateless wichtig?</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request 1: Client → Load-Balancer → Server 1 (State gespeichert)
Request 2: Client → Load-Balancer → Server 2 (State fehlt! Problem!)
</code></pre></div></div>

<p>Mit stateless:</p>
<ul>
  <li>Jeder Server ist identisch</li>
  <li>Jede Anfrage kann an jeden Server gehen</li>
  <li>Keine Abhängigkeit zwischen Anfragen</li>
</ul>

<p><strong>2. Shared Database</strong></p>

<p><strong>Warum eine gemeinsame Datenbank?</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server 1 erstellt: short.ly/abc123 → https://example.com
Server 2 erstellt: short.ly/xyz789 → https://test.com
Server 3 muss beide URLs kennen können!
</code></pre></div></div>

<p><strong>Ohne Shared Database (jeder Server eigene DB):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server 1 DB: abc123 → example.com
Server 2 DB: xyz789 → test.com
Server 3 DB: (leer)

Problem: Server 3 kennt abc123 und xyz789 nicht!
</code></pre></div></div>

<p><strong>Mit Shared Database:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alle Server → Gemeinsame Datenbank
Server 1: Erstellt abc123 → Speichert in DB
Server 2: Erstellt xyz789 → Speichert in DB
Server 3: Kann beide URLs abrufen → Liest aus DB
</code></pre></div></div>

<p><strong>3. Shared Cache (Redis)</strong></p>

<p><strong>Warum ein gemeinsamer Cache?</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server 1: Cache für abc123 → example.com
Server 2: Cache für xyz789 → test.com
Server 3: Kein Cache

Problem: Server 3 muss immer zur Datenbank (langsam)
</code></pre></div></div>

<p><strong>Mit Shared Cache:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alle Server → Gemeinsamer Redis
Server 1: Cache für abc123 → Speichert in Redis
Server 2: Cache für xyz789 → Speichert in Redis
Server 3: Kann beide aus Redis lesen (schnell!)
</code></pre></div></div>

<p><strong>4. Load Balancer</strong></p>

<p><strong>Warum ein Load-Balancer?</strong></p>

<p>Ohne Load-Balancer müssten Clients wissen, welcher Server verfügbar ist:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client muss wählen:
- Soll ich zu Server 1, 2 oder 3 gehen?
- Welcher Server ist verfügbar?
- Wie verteile ich Last gleichmäßig?
</code></pre></div></div>

<p>Mit Load-Balancer:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client → Load-Balancer (wählt automatisch Server) → Server
</code></pre></div></div>

<h4 id="skalierung-in-der-praxis">Skalierung in der Praxis</h4>

<p><strong>Schritt 1: Start mit einem Server</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">api</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">./api</span>
    <span class="c1"># Ein Server, kann ~1.000 Anfragen/Sekunde verarbeiten</span>
</code></pre></div></div>

<p><strong>Schritt 2: Skalierung bei steigender Last</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Docker Compose Skalierung</span>
docker-compose up <span class="nt">-d</span> <span class="nt">--scale</span> <span class="nv">api</span><span class="o">=</span>3

<span class="c"># Jetzt: 3 Server, können ~3.000 Anfragen/Sekunde verarbeiten</span>
</code></pre></div></div>

<p><strong>Schritt 3: Automatische Skalierung (Auto-Scaling)</strong></p>

<p>Bei Cloud-Providern (AWS, Google Cloud, Azure):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Kubernetes Auto-Scaling Beispiel</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">api-autoscaler</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">api</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">50</span>
  <span class="na">metrics</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
    <span class="na">resource</span><span class="pi">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">cpu</span>
      <span class="na">target</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
        <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">70</span>
</code></pre></div></div>

<p><strong>Wie Auto-Scaling funktioniert:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CPU-Auslastung &lt; 50% → Reduziere Server (z.B. 3 → 2)
CPU-Auslastung &gt; 70% → Erhöhe Server (z.B. 3 → 5)
CPU-Auslastung &gt; 90% → Erhöhe Server (z.B. 5 → 10)
</code></pre></div></div>

<p><strong>Vorteile:</strong></p>
<ul>
  <li><strong>Kosteneffizient:</strong> Nur so viele Server wie nötig</li>
  <li><strong>Automatisch:</strong> Keine manuelle Intervention</li>
  <li><strong>Reagiert auf Last:</strong> Skaliert bei Traffic-Spitzen hoch</li>
</ul>

<h4 id="skalierungs-limits-und-bottlenecks">Skalierungs-Limits und Bottlenecks</h4>

<p><strong>Wichtige Überlegung: Wo sind die Limits?</strong></p>

<p><strong>1. API-Server (horizontale Skalierung möglich)</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 Server → 3 Server → 10 Server → 50 Server → 100 Server
✅ Praktisch unbegrenzt skalierbar
</code></pre></div></div>

<p><strong>2. Load-Balancer (kann selbst skaliert werden)</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 Load-Balancer → 2 Load-Balancer (Active-Passive)
✅ Kann auch skaliert werden
</code></pre></div></div>

<p><strong>3. Redis (kann zu Cluster erweitert werden)</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 Redis → Redis-Cluster (3-6 Nodes)
✅ Kann skaliert werden (Sharding)
</code></pre></div></div>

<p><strong>4. Datenbank (kritischer Bottleneck!)</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 Datenbank → Read-Replicas (für Reads)
            → Master (für Writes)
⚠️ Writes sind schwerer zu skalieren
</code></pre></div></div>

<p><strong>Datenbank-Skalierung:</strong></p>

<p><strong>Read-Replicas für bessere Read-Performance:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Master DB (Writes) → Replica 1 (Reads)
                  → Replica 2 (Reads)
                  → Replica 3 (Reads)

API-Server können Reads auf Replicas verteilen
</code></pre></div></div>

<p><strong>Sharding für sehr große Datenmengen:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Shard 1: URLs a-m (20% der Daten)
Shard 2: URLs n-z (30% der Daten)
Shard 3: URLs 0-9 (50% der Daten)

Jeder Shard auf separatem Server
</code></pre></div></div>

<h4 id="skalierungsschritte-praktisches-beispiel">Skalierungsschritte: Praktisches Beispiel</h4>

<p><strong>Phase 1: MVP (1-100 Anfragen/Sekunde)</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1 API-Server</span>
docker-compose up <span class="nt">-d</span>
</code></pre></div></div>

<p><strong>Phase 2: Wachstum (100-1.000 Anfragen/Sekunde)</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 3 API-Server</span>
docker-compose up <span class="nt">-d</span> <span class="nt">--scale</span> <span class="nv">api</span><span class="o">=</span>3
</code></pre></div></div>

<p><strong>Phase 3: Skalierung (1.000-10.000 Anfragen/Sekunde)</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 10 API-Server</span>
docker-compose up <span class="nt">-d</span> <span class="nt">--scale</span> <span class="nv">api</span><span class="o">=</span>10

<span class="c"># Redis-Cluster für bessere Cache-Performance</span>
<span class="c"># Datenbank Read-Replicas für bessere DB-Performance</span>
</code></pre></div></div>

<p><strong>Phase 4: Enterprise (10.000+ Anfragen/Sekunde)</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 50+ API-Server</span>
<span class="c"># Redis-Cluster mit 6 Nodes</span>
<span class="c"># Datenbank mit Sharding</span>
<span class="c"># CDN für statische Inhalte</span>
<span class="c"># Multi-Region Deployment</span>
</code></pre></div></div>

<h4 id="monitoring-der-skalierung">Monitoring der Skalierung</h4>

<p><strong>Wichtige Metriken zum Überwachen:</strong></p>

<ol>
  <li><strong>Request-Rate:</strong> Anfragen pro Sekunde pro Server</li>
  <li><strong>Response-Time:</strong> Durchschnittliche Antwortzeit</li>
  <li><strong>CPU-Auslastung:</strong> Sollte &lt; 70% sein für Puffer</li>
  <li><strong>Memory-Auslastung:</strong> Sollte &lt; 80% sein</li>
  <li><strong>Error-Rate:</strong> Sollte &lt; 1% sein</li>
  <li><strong>Cache-Hit-Rate:</strong> Sollte &gt; 95% sein</li>
</ol>

<p><strong>Wann sollte skaliert werden?</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CPU &gt; 70% für &gt; 5 Minuten → Skaliere hoch
Response-Time &gt; 200ms → Skaliere hoch
Error-Rate &gt; 1% → Skaliere hoch
Cache-Hit-Rate &lt; 90% → Prüfe Redis-Konfiguration
</code></pre></div></div>

<h3 id="7-erweiterte-optimierungen">7. Erweiterte Optimierungen</h3>

<p><strong>Datenbank-Optimierung:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Partitionierung für große Tabellen</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">url_mappings_2025</span> <span class="k">PARTITION</span> <span class="k">OF</span> <span class="n">url_mappings</span>
<span class="k">FOR</span> <span class="k">VALUES</span> <span class="k">FROM</span> <span class="p">(</span><span class="s1">'2025-01-01'</span><span class="p">)</span> <span class="k">TO</span> <span class="p">(</span><span class="s1">'2026-01-01'</span><span class="p">);</span>

<span class="c1">-- Read Replicas für bessere Lesepfad-Performance</span>
</code></pre></div></div>

<p><strong>Caching-Optimierung:</strong></p>

<ul>
  <li><strong>Hot URLs:</strong> Häufig aufgerufene URLs länger cachen</li>
  <li><strong>Bloom Filter:</strong> Schnelle Prüfung ob Short-Code existiert (vor DB-Query)</li>
</ul>

<p><strong>Monitoring:</strong></p>

<ul>
  <li>Redis Hit-Rate überwachen</li>
  <li>Datenbank-Query-Performance tracken</li>
  <li>API-Response-Zeiten messen</li>
  <li>Load Balancer-Metriken beobachten</li>
</ul>

<h2 id="zusammenfassung">Zusammenfassung</h2>

<p>Dieses System-Design bietet:</p>

<p>✅ <strong>Skalierbarkeit:</strong> Horizontale Skalierung durch stateless API-Server<br />
✅ <strong>Performance:</strong> Redis-Caching für schnelle Antwortzeiten<br />
✅ <strong>Zuverlässigkeit:</strong> Load Balancing verteilt Last gleichmäßig<br />
✅ <strong>Einfachheit:</strong> Klare Trennung der Komponenten</p>

<p>Mit dieser Architektur können Sie Millionen von Short-URLs verwalten und tausende von Anfragen pro Sekunde verarbeiten.</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="DevOps" /><summary type="html"><![CDATA[Ich möchte hier anhand eines Beispiels einer einfachen URL Shortener erklären wie man eine moderne Systemarchitektur plant.]]></summary></entry><entry><title type="html">MedusaJS: Ein komplettes Setup-Tutorial für den Einstieg</title><link href="https://docs.pazdzewicz.de/medusa/e-commerce/online-shop/2025/11/30/medusa-setup.html" rel="alternate" type="text/html" title="MedusaJS: Ein komplettes Setup-Tutorial für den Einstieg" /><published>2025-11-30T12:00:00+00:00</published><updated>2025-11-30T12:00:00+00:00</updated><id>https://docs.pazdzewicz.de/medusa/e-commerce/online-shop/2025/11/30/medusa-setup</id><content type="html" xml:base="https://docs.pazdzewicz.de/medusa/e-commerce/online-shop/2025/11/30/medusa-setup.html"><![CDATA[<p>MedusaJS ist ein leistungsstarkes, Open-Source E-Commerce-Framework, das dir die Flexibilität bietet, deinen eigenen Online-Shop zu erstellen. In diesem Tutorial zeige ich dir, wie du MedusaJS von Grund auf einrichtest und deine erste E-Commerce-Anwendung zum Laufen bringst.</p>

<h2 id="was-ist-medusajs">Was ist MedusaJS?</h2>

<p>MedusaJS ist ein modulares E-Commerce-Framework, das aus einem Node.js-Server und einem Admin-Dashboard besteht. Es bietet dir die Möglichkeit, einen vollständig anpassbaren Online-Shop zu erstellen, ohne dabei auf die Flexibilität verzichten zu müssen. Das Framework ist headless, was bedeutet, dass du dein eigenes Storefront mit deiner bevorzugten Technologie erstellen kannst.</p>

<h2 id="voraussetzungen">Voraussetzungen</h2>

<p>Bevor wir mit der Installation beginnen, stelle sicher, dass du folgende Tools auf deinem System installiert hast:</p>

<ul>
  <li><strong>Node.js v20+ (LTS Version)</strong> - <a href="https://nodejs.org/en/download">Download</a></li>
  <li><strong>Git CLI</strong> - <a href="https://git-scm.com/downloads">Download</a></li>
  <li><strong>PostgreSQL</strong> - <a href="https://www.postgresql.org/download/">Download</a></li>
</ul>

<h3 id="postgresql-installation-prüfen">PostgreSQL Installation prüfen</h3>

<p>Um zu überprüfen, ob PostgreSQL installiert ist, führe folgenden Befehl aus:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psql <span class="nt">--version</span>
</code></pre></div></div>

<p>Falls PostgreSQL noch nicht installiert ist, installiere es für dein Betriebssystem. Auf Ubuntu/Debian kannst du es mit folgendem Befehl installieren:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install </span>postgresql postgresql-contrib
</code></pre></div></div>

<h2 id="installation-der-schnelle-weg">Installation: Der schnelle Weg</h2>

<p>Die einfachste Methode, eine Medusa-Anwendung zu erstellen, ist die Verwendung des offiziellen <code class="language-plaintext highlighter-rouge">create-medusa-app</code> Tools. Dieses Tool richtet automatisch alles für dich ein, inklusive der Datenbankverbindung.</p>

<h3 id="schritt-1-medusa-anwendung-erstellen">Schritt 1: Medusa-Anwendung erstellen</h3>

<p>Öffne dein Terminal und führe folgenden Befehl aus:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx create-medusa-app@latest my-medusa-store
</code></pre></div></div>

<p>Ersetze <code class="language-plaintext highlighter-rouge">my-medusa-store</code> mit dem Namen deines Projekts. Der Installer wird dich nach einigen Informationen fragen:</p>

<ol>
  <li><strong>Projektname</strong>: Der Name deines Projekts (wird auch als Datenbankname verwendet)</li>
  <li><strong>Next.js Starter Storefront</strong>: Möchtest du das vorkonfigurierte Next.js Storefront installieren? (Ja/Nein)</li>
</ol>

<p>Für den Anfang empfehle ich, mit “Nein” zu beginnen, damit du die Grundlagen der Medusa-Installation verstehst. Du kannst das Storefront später jederzeit hinzufügen.</p>

<h3 id="schritt-2-installation-abwarten">Schritt 2: Installation abwarten</h3>

<p>Der Installationsprozess kann einige Minuten dauern. Das Tool wird:</p>

<ul>
  <li>Die Medusa-Anwendung in einem Verzeichnis mit deinem Projektnamen installieren</li>
  <li>Eine PostgreSQL-Datenbank für das Projekt einrichten</li>
  <li>Alle notwendigen Abhängigkeiten installieren</li>
  <li>Die Datenbank migrieren</li>
</ul>

<h3 id="schritt-3-erfolgreiche-installation">Schritt 3: Erfolgreiche Installation</h3>

<p>Nach erfolgreicher Installation sollte:</p>

<ul>
  <li>Die Medusa-Anwendung auf <code class="language-plaintext highlighter-rouge">http://localhost:9000</code> laufen</li>
  <li>Das Medusa Admin Dashboard auf <code class="language-plaintext highlighter-rouge">http://localhost:9000/app</code> verfügbar sein</li>
  <li>Der Installer automatisch das Admin Dashboard in deinem Browser öffnen</li>
</ul>

<h3 id="schritt-4-admin-user-initialisieren">Schritt 4: Admin-User initialisieren</h3>

<p>Nach dem Setup musst du den Admin-User initialisieren. Dies kannst du entweder über die Web-Oberfläche machen (beim ersten Öffnen des Admin Dashboards) oder über die Kommandozeile.</p>

<h4 id="option-1-über-die-web-oberfläche">Option 1: Über die Web-Oberfläche</h4>

<p>Beim ersten Öffnen des Admin Dashboards wirst du aufgefordert, einen Admin-Benutzer zu erstellen. Gib die Anmeldedaten ein und klicke auf “Erstellen”.</p>

<h4 id="option-2-über-die-kommandozeile-manuell">Option 2: Über die Kommandozeile (manuell)</h4>

<p>Alternativ kannst du den Admin-User auch direkt über die Kommandozeile erstellen. Navigiere dazu in das Projektverzeichnis und führe folgenden Befehl aus:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>my-medusa-store
npx medusa user <span class="nt">-e</span> user@email.de <span class="nt">-p</span> SuperSecretPassword
</code></pre></div></div>

<p>Ersetze:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">user@email.de</code> mit deiner gewünschten E-Mail-Adresse</li>
  <li><code class="language-plaintext highlighter-rouge">SuperSecretPassword</code> mit deinem gewünschten Passwort</li>
</ul>

<p><strong>Wichtig</strong>: Der Medusa-Server muss laufen, damit dieser Befehl funktioniert. Stelle sicher, dass der Entwicklungsserver gestartet ist.</p>

<p>Nach erfolgreicher Erstellung des Admin-Users kannst du dich im Admin Dashboard unter <code class="language-plaintext highlighter-rouge">http://localhost:9000/app</code> anmelden.</p>

<h2 id="entwicklungsserver-starten">Entwicklungsserver starten</h2>

<p>Um den Entwicklungsserver nach der Installation zu starten, navigiere in das Projektverzeichnis und führe folgenden Befehl aus:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>my-medusa-store
npm run dev
</code></pre></div></div>

<p>Oder wenn du Yarn verwendest:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn dev
</code></pre></div></div>

<p>Der Server startet auf <code class="language-plaintext highlighter-rouge">http://localhost:9000</code> und das Admin Dashboard ist unter <code class="language-plaintext highlighter-rouge">http://localhost:9000/app</code> erreichbar.</p>

<h2 id="projektstruktur-verstehen">Projektstruktur verstehen</h2>

<p>Nach der Installation solltest du folgende Verzeichnisstruktur sehen:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my-medusa-store/
├── src/
│   ├── api/          # API-Routen
│   ├── models/       # Datenbankmodelle
│   ├── services/     # Business-Logik
│   └── subscribers/  # Event-Subscriber
├── medusa-config.ts  # Hauptkonfigurationsdatei
├── package.json
└── .env              # Umgebungsvariablen
</code></pre></div></div>

<h3 id="wichtige-dateien">Wichtige Dateien</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">medusa-config.ts</code></strong>: Die Hauptkonfigurationsdatei für deine Medusa-Anwendung</li>
  <li><strong><code class="language-plaintext highlighter-rouge">.env</code></strong>: Enthält sensible Daten wie Datenbankverbindungen und API-Keys</li>
  <li><strong><code class="language-plaintext highlighter-rouge">src/api/</code></strong>: Hier definierst du deine benutzerdefinierten API-Endpunkte</li>
</ul>

<h2 id="konfiguration-anpassen">Konfiguration anpassen</h2>

<h3 id="cors-einstellungen">CORS-Einstellungen</h3>

<p>Wenn du später ein eigenes Storefront entwickelst, musst du die CORS-Einstellungen in der <code class="language-plaintext highlighter-rouge">.env</code> Datei anpassen:</p>

<pre><code class="language-env">STORE_CORS=http://localhost:3000
AUTH_CORS=http://localhost:5173,http://localhost:9000,http://localhost:3000
</code></pre>

<p>Füge die URLs deines Storefronts zu <code class="language-plaintext highlighter-rouge">STORE_CORS</code> hinzu, damit die Medusa API Anfragen von deinem Storefront akzeptiert.</p>

<h3 id="datenbankverbindung">Datenbankverbindung</h3>

<p>Die Datenbankverbindung wird automatisch während der Installation konfiguriert. Du findest die Verbindungsdetails in der <code class="language-plaintext highlighter-rouge">.env</code> Datei:</p>

<pre><code class="language-env">DATABASE_URL=postgres://postgres:postgres@localhost:5432/my-medusa-store
</code></pre>

<h2 id="alternative-installation-mit-docker">Alternative: Installation mit Docker</h2>

<p>Falls du Docker bevorzugst oder Probleme mit der lokalen PostgreSQL-Installation hast, kannst du Medusa auch mit Docker einrichten.</p>

<h3 id="voraussetzungen-für-docker">Voraussetzungen für Docker</h3>

<ul>
  <li><a href="https://docs.docker.com/get-docker/">Docker</a></li>
  <li><a href="https://docs.docker.com/compose/install/">Docker Compose</a></li>
</ul>

<h3 id="schritt-1-repository-klonen">Schritt 1: Repository klonen</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/medusajs/medusa-starter-default.git <span class="nt">--depth</span><span class="o">=</span>1 my-medusa-store
<span class="nb">cd </span>my-medusa-store
</code></pre></div></div>

<h3 id="schritt-2-docker-compose-erstellen">Schritt 2: Docker Compose erstellen</h3>

<p>Erstelle eine <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Datei:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">postgres</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:15-alpine</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">medusa_postgres</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">medusa-store</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">postgres_data:/var/lib/postgresql/data</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">medusa_network</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">postgres_data</span><span class="pi">:</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">medusa_network</span><span class="pi">:</span>
</code></pre></div></div>

<h3 id="schritt-3-dependencies-installieren">Schritt 3: Dependencies installieren</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">--legacy-peer-deps</span>
</code></pre></div></div>

<h3 id="schritt-4-docker-container-starten">Schritt 4: Docker Container starten</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose up <span class="nt">-d</span>
</code></pre></div></div>

<h3 id="schritt-5-medusa-konfiguration-anpassen">Schritt 5: Medusa-Konfiguration anpassen</h3>

<p>In <code class="language-plaintext highlighter-rouge">medusa-config.ts</code> musst du die SSL-Einstellungen für die Datenbankverbindung anpassen:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">loadEnv</span><span class="p">,</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@medusajs/framework/utils</span><span class="dl">"</span>

<span class="nf">loadEnv</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">development</span><span class="dl">"</span><span class="p">,</span> <span class="nx">process</span><span class="p">.</span><span class="nf">cwd</span><span class="p">())</span>

<span class="kr">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nf">defineConfig</span><span class="p">({</span>
  <span class="na">projectConfig</span><span class="p">:</span> <span class="p">{</span>
    <span class="c1">// ... andere Konfigurationen</span>
    <span class="na">databaseDriverOptions</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">ssl</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">sslmode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">disable</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<h3 id="schritt-6-admin-user-initialisieren">Schritt 6: Admin-User initialisieren</h3>

<p>Nach dem Setup musst du den Admin-User manuell initialisieren. Dies ist notwendig, damit du dich im Admin Dashboard anmelden kannst.</p>

<p>Führe folgenden Befehl aus, um einen Admin-User zu erstellen:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nb">exec </span>medusa npx medusa user <span class="nt">-e</span> user@email.de <span class="nt">-p</span> SuperSecretPassword
</code></pre></div></div>

<p>Ersetze:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">user@email.de</code> mit deiner gewünschten E-Mail-Adresse</li>
  <li><code class="language-plaintext highlighter-rouge">SuperSecretPassword</code> mit deinem gewünschten Passwort</li>
</ul>

<p><strong>Wichtig</strong>: Der Medusa-Container muss laufen, damit dieser Befehl funktioniert. Stelle sicher, dass alle Container gestartet sind (<code class="language-plaintext highlighter-rouge">docker compose up -d</code>).</p>

<p>Nach erfolgreicher Erstellung des Admin-Users kannst du dich im Admin Dashboard unter <code class="language-plaintext highlighter-rouge">http://localhost:9000/app</code> anmelden.</p>

<h2 id="erste-schritte-im-admin-dashboard">Erste Schritte im Admin Dashboard</h2>

<p>Nachdem du dich im Admin Dashboard angemeldet hast, kannst du:</p>

<ol>
  <li><strong>Produkte erstellen</strong>: Navigiere zu “Products” und füge dein erstes Produkt hinzu</li>
  <li><strong>Regionen konfigurieren</strong>: Richte Versandregionen und Steuersätze ein</li>
  <li><strong>Zahlungsmethoden einrichten</strong>: Konfiguriere Zahlungsanbieter wie Stripe</li>
  <li><strong>Versandoptionen</strong>: Definiere Versandmethoden für deine Regionen</li>
</ol>

<h2 id="häufige-probleme-und-lösungen">Häufige Probleme und Lösungen</h2>

<h3 id="problem-installation-schlägt-fehl">Problem: Installation schlägt fehl</h3>

<p><strong>Lösung</strong>: Stelle sicher, dass:</p>
<ul>
  <li>Node.js v20 oder höher installiert ist</li>
  <li>PostgreSQL läuft und erreichbar ist</li>
  <li>Genügend Festplattenspeicher verfügbar ist</li>
</ul>

<h3 id="problem-cors-fehler">Problem: CORS-Fehler</h3>

<p><strong>Lösung</strong>: Überprüfe die CORS-Einstellungen in deiner <code class="language-plaintext highlighter-rouge">.env</code> Datei und füge die URL deines Storefronts hinzu.</p>

<h3 id="problem-datenbankverbindungsfehler">Problem: Datenbankverbindungsfehler</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe, ob PostgreSQL läuft: <code class="language-plaintext highlighter-rouge">sudo systemctl status postgresql</code></li>
  <li>Überprüfe die <code class="language-plaintext highlighter-rouge">DATABASE_URL</code> in der <code class="language-plaintext highlighter-rouge">.env</code> Datei</li>
  <li>Stelle sicher, dass die Datenbank existiert</li>
</ul>

<h3 id="problem-port-bereits-belegt">Problem: Port bereits belegt</h3>

<p><strong>Lösung</strong>: Ändere den Port in der <code class="language-plaintext highlighter-rouge">medusa-config.ts</code> oder beende den Prozess, der den Port verwendet.</p>

<h2 id="nächste-schritte">Nächste Schritte</h2>

<p>Nach erfolgreicher Installation kannst du:</p>

<ol>
  <li><strong>Storefront entwickeln</strong>: Erstelle dein eigenes Storefront mit Next.js, React oder einer anderen Technologie</li>
  <li><strong>Plugins installieren</strong>: Erweitere Medusa mit offiziellen oder Community-Plugins</li>
  <li><strong>API anpassen</strong>: Passe die API-Endpunkte an deine Bedürfnisse an</li>
  <li><strong>Custom Services</strong>: Erstelle eigene Services für spezielle Business-Logik</li>
</ol>

<h2 id="fazit">Fazit</h2>

<p>MedusaJS bietet eine solide Grundlage für dein E-Commerce-Projekt. Mit diesem Setup hast du eine vollständig funktionsfähige Backend-Infrastruktur, die du nach deinen Wünschen anpassen kannst. Die modulare Architektur ermöglicht es dir, genau die Features zu implementieren, die du benötigst, ohne unnötigen Ballast.</p>

<p>Falls du Fragen hast oder auf Probleme stößt, schaue in die <a href="https://docs.medusajs.com">offizielle Medusa-Dokumentation</a> oder die <a href="https://docs.medusajs.com/resources/troubleshooting">Troubleshooting-Guides</a>.</p>

<p>Viel Erfolg mit deinem MedusaJS-Projekt! 🚀</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="medusa" /><category term="e-commerce" /><category term="online-shop" /><summary type="html"><![CDATA[MedusaJS ist ein leistungsstarkes, Open-Source E-Commerce-Framework, das dir die Flexibilität bietet, deinen eigenen Online-Shop zu erstellen. In diesem Tutorial zeige ich dir, wie du MedusaJS von Grund auf einrichtest und deine erste E-Commerce-Anwendung zum Laufen bringst.]]></summary></entry><entry><title type="html">MedusaJS: Stripe Payment Integration</title><link href="https://docs.pazdzewicz.de/medusa/e-commerce/online-shop/2025/11/30/medusa-stripe.html" rel="alternate" type="text/html" title="MedusaJS: Stripe Payment Integration" /><published>2025-11-30T12:00:00+00:00</published><updated>2025-11-30T12:00:00+00:00</updated><id>https://docs.pazdzewicz.de/medusa/e-commerce/online-shop/2025/11/30/medusa-stripe</id><content type="html" xml:base="https://docs.pazdzewicz.de/medusa/e-commerce/online-shop/2025/11/30/medusa-stripe.html"><![CDATA[<p>Stripe ist einer der beliebtesten Payment Provider für E-Commerce-Anwendungen. In diesem Tutorial zeige ich dir, wie du Stripe nahtlos in deine MedusaJS-Anwendung integrierst und Zahlungen verarbeitest.</p>

<h2 id="warum-stripe-mit-medusajs">Warum Stripe mit MedusaJS?</h2>

<p>Stripe bietet eine robuste, sichere und benutzerfreundliche Lösung für die Zahlungsabwicklung. Die Integration mit MedusaJS ermöglicht es dir:</p>

<ul>
  <li><strong>Kreditkartenzahlungen</strong> zu akzeptieren</li>
  <li><strong>Alternative Zahlungsmethoden</strong> wie Bancontact, iDEAL, giropay und mehr anzubieten</li>
  <li><strong>Automatische Zahlungsabwicklung</strong> mit Webhooks</li>
  <li><strong>Gespeicherte Zahlungsmethoden</strong> für wiederkehrende Kunden zu unterstützen</li>
  <li><strong>Apple Pay und Google Pay</strong> zu integrieren</li>
</ul>

<h2 id="voraussetzungen">Voraussetzungen</h2>

<p>Bevor wir mit der Integration beginnen, stelle sicher, dass du:</p>

<ol>
  <li><strong>Eine funktionierende MedusaJS-Anwendung</strong> hast (siehe <a href="./medusa-setup.html">MedusaJS Setup Tutorial</a>)</li>
  <li><strong>Ein Stripe-Konto</strong> besitzt - <a href="https://stripe.com/">Registrierung</a></li>
  <li><strong>Zugriff auf deine Stripe API Keys</strong> hast</li>
</ol>

<h3 id="stripe-api-keys-abrufen">Stripe API Keys abrufen</h3>

<ol>
  <li>Logge dich in dein <a href="https://dashboard.stripe.com/">Stripe Dashboard</a> ein</li>
  <li>Navigiere zu <strong>Developers &gt; API keys</strong></li>
  <li>Du findest dort:
    <ul>
      <li><strong>Publishable key</strong> (beginnt mit <code class="language-plaintext highlighter-rouge">pk_test_</code> oder <code class="language-plaintext highlighter-rouge">pk_live_</code>)</li>
      <li><strong>Secret key</strong> (beginnt mit <code class="language-plaintext highlighter-rouge">sk_test_</code> oder <code class="language-plaintext highlighter-rouge">sk_live_</code>)</li>
    </ul>
  </li>
</ol>

<p><strong>Wichtig</strong>: Für die Entwicklung verwende die <strong>Test Keys</strong> (mit <code class="language-plaintext highlighter-rouge">_test_</code>). Für Production benötigst du die <strong>Live Keys</strong> (mit <code class="language-plaintext highlighter-rouge">_live_</code>).</p>

<h2 id="schritt-1-stripe-module-provider-registrieren">Schritt 1: Stripe Module Provider registrieren</h2>

<p>Der Stripe Module Provider ist standardmäßig in deiner Medusa-Anwendung installiert. Um ihn zu aktivieren, musst du ihn in der Konfigurationsdatei registrieren.</p>

<h3 id="medusa-configts-anpassen">medusa-config.ts anpassen</h3>

<p>Öffne die <code class="language-plaintext highlighter-rouge">medusa-config.ts</code> Datei in deinem Medusa-Projekt und füge den Stripe Module Provider hinzu:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nf">defineConfig</span><span class="p">({</span>
  <span class="c1">// ... andere Konfigurationen</span>
  <span class="na">modules</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">resolve</span><span class="p">:</span> <span class="dl">"</span><span class="s2">@medusajs/medusa/payment</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">providers</span><span class="p">:</span> <span class="p">[</span>
          <span class="p">{</span>
            <span class="na">resolve</span><span class="p">:</span> <span class="dl">"</span><span class="s2">@medusajs/medusa/payment-stripe</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">stripe</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
              <span class="na">apiKey</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">STRIPE_API_KEY</span><span class="p">,</span>
            <span class="p">},</span>
          <span class="p">},</span>
        <span class="p">],</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">],</span>
<span class="p">})</span>
</code></pre></div></div>

<p><strong>Wichtig</strong>: Auch wenn das Payment Module standardmäßig geladen wird, musst du es erneut in der <code class="language-plaintext highlighter-rouge">modules</code>-Array hinzufügen, wenn du einen neuen Provider registrierst.</p>

<h3 id="erweiterte-konfigurationsoptionen">Erweiterte Konfigurationsoptionen</h3>

<p>Der Stripe Module Provider unterstützt weitere Optionen:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">resolve</span><span class="p">:</span> <span class="dl">"</span><span class="s2">@medusajs/medusa/payment-stripe</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">stripe</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">apiKey</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">STRIPE_API_KEY</span><span class="p">,</span>
    <span class="nx">webhookSecret</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">STRIPE_WEBHOOK_SECRET</span><span class="p">,</span> <span class="c1">// Für Production</span>
    <span class="nx">automaticPaymentMethods</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// Für Apple Pay, Google Pay</span>
    <span class="nx">capture</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// Automatische Zahlungserfassung</span>
    <span class="nx">paymentDescription</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Bestellung bei meinem Shop</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// Standard-Beschreibung</span>
  <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Optionen im Detail:</strong></p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">apiKey</code></strong> (erforderlich): Dein Stripe Secret API Key</li>
  <li><strong><code class="language-plaintext highlighter-rouge">webhookSecret</code></strong> (optional, für Production): Webhook Secret für sichere Webhook-Verarbeitung</li>
  <li><strong><code class="language-plaintext highlighter-rouge">automaticPaymentMethods</code></strong> (optional): Aktiviert automatische Zahlungsmethoden wie Apple Pay und Google Pay</li>
  <li><strong><code class="language-plaintext highlighter-rouge">capture</code></strong> (optional): Ob Zahlungen automatisch erfasst werden sollen (Standard: <code class="language-plaintext highlighter-rouge">false</code>)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">paymentDescription</code></strong> (optional): Standard-Beschreibung für Zahlungen</li>
</ul>

<h2 id="schritt-2-environment-variables-setzen">Schritt 2: Environment Variables setzen</h2>

<p>Füge deine Stripe API Keys zur <code class="language-plaintext highlighter-rouge">.env</code> Datei hinzu:</p>

<pre><code class="language-env">STRIPE_API_KEY=sk_test_51J...
</code></pre>

<p><strong>Für Production</strong> füge auch das Webhook Secret hinzu:</p>

<pre><code class="language-env">STRIPE_API_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
</code></pre>

<p><strong>Sicherheitshinweis</strong>: Stelle sicher, dass deine <code class="language-plaintext highlighter-rouge">.env</code> Datei in <code class="language-plaintext highlighter-rouge">.gitignore</code> enthalten ist und niemals in dein Repository committed wird!</p>

<h2 id="schritt-3-stripe-in-einer-region-aktivieren">Schritt 3: Stripe in einer Region aktivieren</h2>

<p>Nach der Konfiguration musst du Stripe als Zahlungsanbieter in mindestens einer Region aktivieren. Kunden können nur Zahlungsmethoden verwenden, die in ihrer Region verfügbar sind.</p>

<h3 id="über-das-admin-dashboard">Über das Admin Dashboard</h3>

<ol>
  <li>Starte deine Medusa-Anwendung:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run dev
</code></pre></div></div>

<ol>
  <li>Öffne das Admin Dashboard unter <code class="language-plaintext highlighter-rouge">http://localhost:9000/app</code></li>
  <li>Logge dich mit deinem Admin-Account ein</li>
  <li>Navigiere zu <strong>Settings &gt; Regions</strong></li>
  <li>Klicke auf die Region, in der du Stripe aktivieren möchtest</li>
  <li>Klicke auf das <strong>Bearbeiten-Icon</strong> (oben rechts im ersten Abschnitt)</li>
  <li>Im sich öffnenden Seitenfenster findest du das Feld <strong>“Payment Providers”</strong></li>
  <li>Wähle <strong>“Stripe (STRIPE)”</strong> aus dem Dropdown-Menü</li>
  <li>Klicke auf <strong>“Save”</strong></li>
</ol>

<p>Stripe ist jetzt als Zahlungsoption im Checkout verfügbar!</p>

<h2 id="schritt-4-verfügbare-stripe-zahlungsmethoden">Schritt 4: Verfügbare Stripe-Zahlungsmethoden</h2>

<p>Der Stripe Module Provider registriert verschiedene Zahlungsanbieter. Jeder Provider hat eine eindeutige ID im Format <code class="language-plaintext highlighter-rouge">{provider_id}_{module_id}</code>.</p>

<p>Wenn du die Stripe Module Provider ID auf <code class="language-plaintext highlighter-rouge">"stripe"</code> gesetzt hast, werden folgende Provider registriert:</p>

<table>
  <thead>
    <tr>
      <th>Provider Name</th>
      <th>Provider ID</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Basic Stripe Payment</td>
      <td><code class="language-plaintext highlighter-rouge">stripe_stripe</code></td>
    </tr>
    <tr>
      <td>Bancontact Payments</td>
      <td><code class="language-plaintext highlighter-rouge">bancontact_stripe</code></td>
    </tr>
    <tr>
      <td>BLIK Payments</td>
      <td><code class="language-plaintext highlighter-rouge">blik_stripe</code></td>
    </tr>
    <tr>
      <td>giropay Payments</td>
      <td><code class="language-plaintext highlighter-rouge">giropay_stripe</code></td>
    </tr>
    <tr>
      <td>iDEAL Payments</td>
      <td><code class="language-plaintext highlighter-rouge">ideal_stripe</code></td>
    </tr>
    <tr>
      <td>Przelewy24 Payments</td>
      <td><code class="language-plaintext highlighter-rouge">przelewy24_stripe</code></td>
    </tr>
    <tr>
      <td>PromptPay Payments</td>
      <td><code class="language-plaintext highlighter-rouge">promptpay_stripe</code></td>
    </tr>
  </tbody>
</table>

<p>Du kannst jeden dieser Provider separat in deinen Regionen aktivieren, je nachdem, welche Zahlungsmethoden du anbieten möchtest.</p>

<h2 id="schritt-5-webhooks-für-production-konfigurieren">Schritt 5: Webhooks für Production konfigurieren</h2>

<p>Für Production-Anwendungen ist es essentiell, Stripe Webhooks einzurichten. Webhooks informieren deine Medusa-Anwendung über Änderungen und Updates zu Zahlungen, die asynchron verarbeitet werden.</p>

<h3 id="warum-webhooks-wichtig-sind">Warum Webhooks wichtig sind</h3>

<p>Webhooks sind wichtig, wenn:</p>
<ul>
  <li>Zahlungen asynchron verarbeitet werden</li>
  <li>Zahlungen auf der Stripe-Seite verwaltet werden</li>
  <li>Der Checkout-Prozess unterbrochen wurde und die Zahlung trotzdem verarbeitet wurde</li>
</ul>

<h3 id="webhook-url">Webhook URL</h3>

<p>Die Webhook-URL für MedusaJS folgt diesem Format:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://deine-domain.com/hooks/payment/{provider_id}/stripe
</code></pre></div></div>

<p>Für die Standard-Stripe-Zahlung wäre die URL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://deine-domain.com/hooks/payment/stripe_stripe/stripe
</code></pre></div></div>

<p><strong>Für verschiedene Zahlungsmethoden:</strong></p>

<table>
  <thead>
    <tr>
      <th>Stripe Payment Type</th>
      <th>Webhook Endpoint URL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Basic Stripe Payment</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/stripe_stripe/stripe</code></td>
    </tr>
    <tr>
      <td>Bancontact Payments</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/bancontact_stripe/stripe</code></td>
    </tr>
    <tr>
      <td>BLIK Payments</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/blik_stripe/stripe</code></td>
    </tr>
    <tr>
      <td>giropay Payments</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/giropay_stripe/stripe</code></td>
    </tr>
    <tr>
      <td>iDEAL Payments</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/ideal_stripe/stripe</code></td>
    </tr>
    <tr>
      <td>Przelewy24 Payments</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/przelewy24_stripe/stripe</code></td>
    </tr>
    <tr>
      <td>PromptPay Payments</td>
      <td><code class="language-plaintext highlighter-rouge">/hooks/payment/promptpay_stripe/stripe</code></td>
    </tr>
  </tbody>
</table>

<h3 id="webhook-in-stripe-einrichten">Webhook in Stripe einrichten</h3>

<ol>
  <li>Gehe zu deinem <a href="https://dashboard.stripe.com/">Stripe Dashboard</a></li>
  <li>Navigiere zu <strong>Developers &gt; Webhooks</strong></li>
  <li>Klicke auf <strong>“Add endpoint”</strong></li>
  <li>Gib deine Webhook-URL ein (z.B. <code class="language-plaintext highlighter-rouge">https://deine-domain.com/hooks/payment/stripe_stripe/stripe</code>)</li>
  <li>Wähle die folgenden Events aus:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">payment_intent.succeeded</code></li>
      <li><code class="language-plaintext highlighter-rouge">payment_intent.payment_failed</code></li>
      <li><code class="language-plaintext highlighter-rouge">charge.succeeded</code></li>
      <li><code class="language-plaintext highlighter-rouge">charge.failed</code> (seit Medusa v2.8.5)</li>
    </ul>
  </li>
  <li>Klicke auf <strong>“Add endpoint”</strong></li>
  <li>Kopiere das <strong>Webhook Secret</strong> (beginnt mit <code class="language-plaintext highlighter-rouge">whsec_</code>)</li>
  <li>Füge es zu deiner <code class="language-plaintext highlighter-rouge">.env</code> Datei hinzu:</li>
</ol>

<pre><code class="language-env">STRIPE_WEBHOOK_SECRET=whsec_...
</code></pre>

<ol>
  <li>Aktualisiere deine <code class="language-plaintext highlighter-rouge">medusa-config.ts</code>:</li>
</ol>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">resolve</span><span class="p">:</span> <span class="dl">"</span><span class="s2">@medusajs/medusa/payment-stripe</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">stripe</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">apiKey</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">STRIPE_API_KEY</span><span class="p">,</span>
    <span class="nx">webhookSecret</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">STRIPE_WEBHOOK_SECRET</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="webhook-lokal-testen">Webhook lokal testen</h3>

<p>Für lokale Tests kannst du <a href="https://stripe.com/docs/stripe-cli">Stripe CLI</a> verwenden:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Stripe CLI installieren</span>
<span class="c"># Dann Webhook weiterleiten</span>
stripe listen <span class="nt">--forward-to</span> localhost:9000/hooks/payment/stripe_stripe/stripe
</code></pre></div></div>

<p>Die CLI zeigt dir ein Webhook Secret an, das du für lokale Tests verwenden kannst.</p>

<h2 id="schritt-6-storefront-integration">Schritt 6: Storefront-Integration</h2>

<p>Wenn du ein eigenes Storefront entwickelst (z.B. mit Next.js), benötigst du auch den Stripe Publishable Key.</p>

<h3 id="environment-variable-im-storefront">Environment Variable im Storefront</h3>

<p>Füge den Publishable Key zu deinen Storefront-Umgebungsvariablen hinzu:</p>

<pre><code class="language-env"># Next.js Storefront
NEXT_PUBLIC_STRIPE_KEY=pk_test_...

# Oder für andere Frameworks
STRIPE_PUBLISHABLE_KEY=pk_test_...
</code></pre>

<p><strong>Wichtig</strong>: Der Publishable Key kann sicher im Frontend verwendet werden, da er nur für die Initialisierung von Stripe-Elementen verwendet wird.</p>

<h2 id="erweiterte-features">Erweiterte Features</h2>

<h3 id="gespeicherte-zahlungsmethoden">Gespeicherte Zahlungsmethoden</h3>

<p>Der Stripe Module Provider unterstützt gespeicherte Zahlungsmethoden. Kunden können ihre Zahlungsinformationen speichern und bei zukünftigen Bestellungen wiederverwenden.</p>

<p>Die Implementierung erfordert zusätzliche Anpassungen im Storefront. Weitere Details findest du in der <a href="https://docs.medusajs.com/resources/how-to-tutorials/tutorials/saved-payment-methods">offiziellen Medusa-Dokumentation</a>.</p>

<h3 id="apple-pay-und-google-pay">Apple Pay und Google Pay</h3>

<p>Um Apple Pay und Google Pay zu aktivieren:</p>

<ol>
  <li>Setze <code class="language-plaintext highlighter-rouge">automaticPaymentMethods: true</code> in der Stripe-Konfiguration</li>
  <li>Implementiere die entsprechenden UI-Elemente in deinem Storefront</li>
  <li>Stelle sicher, dass deine Domain für Apple Pay verifiziert ist</li>
</ol>

<h3 id="zahlungsbeschreibungen-anpassen">Zahlungsbeschreibungen anpassen</h3>

<p>Du kannst benutzerdefinierte Beschreibungen für Zahlungen festlegen:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// In medusa-config.ts</span>
<span class="p">{</span>
  <span class="nl">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">paymentDescription</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Bestellung #{order_number} bei meinem Shop</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Oder dynamisch im Cart Context:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Im Storefront</span>
<span class="kd">const</span> <span class="nx">cart</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">medusa</span><span class="p">.</span><span class="nx">carts</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span>
  <span class="c1">// ... andere Optionen</span>
  <span class="na">context</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">payment_description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Spezielle Bestellung</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<h2 id="häufige-probleme-und-lösungen">Häufige Probleme und Lösungen</h2>

<h3 id="problem-stripe-erscheint-nicht-in-den-payment-providers">Problem: Stripe erscheint nicht in den Payment Providers</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe, ob der Stripe Module Provider korrekt in <code class="language-plaintext highlighter-rouge">medusa-config.ts</code> registriert ist</li>
  <li>Stelle sicher, dass <code class="language-plaintext highlighter-rouge">STRIPE_API_KEY</code> in der <code class="language-plaintext highlighter-rouge">.env</code> Datei gesetzt ist</li>
  <li>Starte den Medusa-Server neu</li>
</ul>

<h3 id="problem-zahlungen-werden-nicht-verarbeitet">Problem: Zahlungen werden nicht verarbeitet</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe, ob Stripe in der Region aktiviert ist</li>
  <li>Stelle sicher, dass du den richtigen API Key verwendest (Test vs. Live)</li>
  <li>Überprüfe die Stripe-Logs im Dashboard auf Fehler</li>
</ul>

<h3 id="problem-webhook-events-werden-nicht-empfangen">Problem: Webhook-Events werden nicht empfangen</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe, ob die Webhook-URL korrekt ist</li>
  <li>Stelle sicher, dass deine Anwendung öffentlich erreichbar ist (für Production)</li>
  <li>Überprüfe, ob das <code class="language-plaintext highlighter-rouge">webhookSecret</code> korrekt gesetzt ist</li>
  <li>Teste die Webhook-URL mit Stripe CLI lokal</li>
</ul>

<h3 id="problem-cors-fehler-im-storefront">Problem: CORS-Fehler im Storefront</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Füge deine Storefront-URL zu <code class="language-plaintext highlighter-rouge">STORE_CORS</code> in der <code class="language-plaintext highlighter-rouge">.env</code> Datei hinzu:</li>
</ul>

<pre><code class="language-env">STORE_CORS=http://localhost:3000,https://deine-domain.com
</code></pre>

<h3 id="problem-ssl-fehler-bei-webhooks">Problem: SSL-Fehler bei Webhooks</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Stelle sicher, dass deine Production-URL HTTPS verwendet</li>
  <li>Überprüfe, ob dein SSL-Zertifikat gültig ist</li>
  <li>Für lokale Tests verwende Stripe CLI</li>
</ul>

<h2 id="testen-der-integration">Testen der Integration</h2>

<h3 id="test-kreditkarten">Test-Kreditkarten</h3>

<p>Stripe bietet Test-Kreditkarten für die Entwicklung:</p>

<ul>
  <li><strong>Erfolgreiche Zahlung</strong>: <code class="language-plaintext highlighter-rouge">4242 4242 4242 4242</code></li>
  <li><strong>Abgelehnte Zahlung</strong>: <code class="language-plaintext highlighter-rouge">4000 0000 0000 0002</code></li>
  <li><strong>3D Secure erforderlich</strong>: <code class="language-plaintext highlighter-rouge">4000 0025 0000 3155</code></li>
</ul>

<p>Weitere Test-Karten findest du in der <a href="https://stripe.com/docs/testing">Stripe-Dokumentation</a>.</p>

<h3 id="zahlungsfluss-testen">Zahlungsfluss testen</h3>

<ol>
  <li>Erstelle ein Produkt im Admin Dashboard</li>
  <li>Füge es zum Warenkorb hinzu</li>
  <li>Gehe zum Checkout</li>
  <li>Wähle Stripe als Zahlungsmethode</li>
  <li>Verwende eine Test-Kreditkarte</li>
  <li>Überprüfe im Stripe Dashboard, ob die Zahlung registriert wurde</li>
</ol>

<h2 id="best-practices">Best Practices</h2>

<ol>
  <li><strong>Sicherheit</strong>:
    <ul>
      <li>Verwende niemals Live Keys in der Entwicklung</li>
      <li>Speichere Secrets sicher (z.B. in Environment Variables)</li>
      <li>Implementiere Rate Limiting für Webhook-Endpunkte</li>
    </ul>
  </li>
  <li><strong>Error Handling</strong>:
    <ul>
      <li>Implementiere umfassendes Error Handling im Storefront</li>
      <li>Zeige benutzerfreundliche Fehlermeldungen</li>
      <li>Logge Fehler für Debugging</li>
    </ul>
  </li>
  <li><strong>Monitoring</strong>:
    <ul>
      <li>Überwache Webhook-Events im Stripe Dashboard</li>
      <li>Setze Alerts für fehlgeschlagene Zahlungen</li>
      <li>Überprüfe regelmäßig die Medusa-Logs</li>
    </ul>
  </li>
  <li><strong>Testing</strong>:
    <ul>
      <li>Teste alle Zahlungsmethoden vor dem Go-Live</li>
      <li>Teste Webhook-Events mit Stripe CLI</li>
      <li>Führe umfassende Integrationstests durch</li>
    </ul>
  </li>
</ol>

<h2 id="nächste-schritte">Nächste Schritte</h2>

<p>Nach erfolgreicher Stripe-Integration kannst du:</p>

<ol>
  <li><strong>Weitere Zahlungsmethoden aktivieren</strong>: Bancontact, iDEAL, giropay, etc.</li>
  <li><strong>Gespeicherte Zahlungsmethoden implementieren</strong>: Für wiederkehrende Kunden</li>
  <li><strong>Subscriptions einrichten</strong>: Für wiederkehrende Zahlungen</li>
  <li><strong>Custom Payment Flows</strong>: Anpassen des Checkout-Prozesses</li>
  <li><strong>Analytics integrieren</strong>: Zahlungsstatistiken tracken</li>
</ol>

<h2 id="fazit">Fazit</h2>

<p>Die Integration von Stripe in MedusaJS ist unkompliziert und bietet dir eine solide Grundlage für die Zahlungsabwicklung in deinem E-Commerce-Shop. Mit diesem Setup kannst du verschiedene Zahlungsmethoden anbieten und sicher Zahlungen verarbeiten.</p>

<p>Falls du Fragen hast oder auf Probleme stößt, schaue in die <a href="https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe">offizielle Medusa-Dokumentation</a> oder die <a href="https://stripe.com/docs">Stripe-Dokumentation</a>.</p>

<p>Viel Erfolg mit deiner Stripe-Integration! 💳</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="medusa" /><category term="e-commerce" /><category term="online-shop" /><summary type="html"><![CDATA[Stripe ist einer der beliebtesten Payment Provider für E-Commerce-Anwendungen. In diesem Tutorial zeige ich dir, wie du Stripe nahtlos in deine MedusaJS-Anwendung integrierst und Zahlungen verarbeitest.]]></summary></entry><entry><title type="html">Matrix Secure Chat Setup</title><link href="https://docs.pazdzewicz.de/matrix/chat/security/2025/09/29/matrix-secure-chat.html" rel="alternate" type="text/html" title="Matrix Secure Chat Setup" /><published>2025-09-29T12:00:00+00:00</published><updated>2025-09-29T12:00:00+00:00</updated><id>https://docs.pazdzewicz.de/matrix/chat/security/2025/09/29/matrix-secure-chat</id><content type="html" xml:base="https://docs.pazdzewicz.de/matrix/chat/security/2025/09/29/matrix-secure-chat.html"><![CDATA[<p>Matrix ist ein dezentrales, offenes Protokoll für sichere, Echtzeit-Kommunikation. Es ermöglicht dir, deinen eigenen Chat-Server zu betreiben, der vollständig unter deiner Kontrolle steht und mit anderen Matrix-Servern kommunizieren kann (Federation). In diesem ausführlichen Tutorial zeige ich dir, wie du einen vollständigen Matrix-Server mit Docker Compose aufsetzt, inklusive Synapse (der Referenz-Server), Element (Web-Client), Traefik (Reverse-Proxy), TURN-Server für Voice/Video und optional WhatsApp-Bridge.</p>

<h2 id="was-ist-matrix">Was ist Matrix?</h2>

<p>Matrix ist ein offenes Protokoll für dezentrale, Echtzeit-Kommunikation. Die wichtigsten Vorteile:</p>

<ul>
  <li><strong>Dezentral</strong>: Jeder kann seinen eigenen Server betreiben</li>
  <li><strong>Federiert</strong>: Server können miteinander kommunizieren (wie E-Mail)</li>
  <li><strong>Ende-zu-Ende verschlüsselt</strong>: Sichere Kommunikation</li>
  <li><strong>Open Source</strong>: Vollständig transparent und anpassbar</li>
  <li><strong>Multi-Client</strong>: Verschiedene Clients für verschiedene Plattformen</li>
  <li><strong>Rich Features</strong>: Text, Voice, Video, Dateien, etc.</li>
</ul>

<h2 id="architektur-übersicht">Architektur-Übersicht</h2>

<p>Dieses Setup umfasst folgende Komponenten:</p>

<ol>
  <li><strong>Synapse</strong>: Der Matrix-Homeserver (Hauptkomponente)</li>
  <li><strong>PostgreSQL</strong>: Datenbank für Synapse</li>
  <li><strong>Element Web</strong>: Web-basierter Chat-Client</li>
  <li><strong>Traefik</strong>: Reverse-Proxy mit automatischem SSL</li>
  <li><strong>Coturn</strong>: TURN-Server für Voice/Video-Calls</li>
  <li><strong>LiveKit</strong>: Moderne WebRTC-Infrastruktur für Video-Calls</li>
  <li><strong>Synapse Admin</strong>: Web-Interface zur Server-Verwaltung</li>
  <li><strong>Mautrix WhatsApp</strong> (optional): Bridge zu WhatsApp</li>
</ol>

<h2 id="voraussetzungen">Voraussetzungen</h2>

<p>Bevor wir mit der Installation beginnen, stelle sicher, dass du folgende Voraussetzungen erfüllst:</p>

<h3 id="systemanforderungen">Systemanforderungen</h3>

<ul>
  <li><strong>Docker</strong> Version 20.10 oder höher - <a href="https://docs.docker.com/get-docker/">Download</a></li>
  <li><strong>Docker Compose</strong> Version 2.0 oder höher - <a href="https://docs.docker.com/compose/install/">Download</a></li>
  <li><strong>Mindestens 2GB RAM</strong> (4GB+ empfohlen)</li>
  <li><strong>Mindestens 10GB freier Speicherplatz</strong></li>
  <li><strong>Eine Domain</strong> mit DNS-Zugriff (für SSL-Zertifikate)</li>
</ul>

<h3 id="installation-prüfen">Installation prüfen</h3>

<p>Überprüfe, ob Docker und Docker Compose installiert sind:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nt">--version</span>
docker compose version
</code></pre></div></div>

<h3 id="dns-konfiguration">DNS-Konfiguration</h3>

<p>Du benötigst eine Domain mit folgenden DNS-Einträgen:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">matrix.deine-domain.de</code> → Deine Server-IP (für Synapse)</li>
  <li><code class="language-plaintext highlighter-rouge">chat.deine-domain.de</code> → Deine Server-IP (für Element)</li>
  <li><code class="language-plaintext highlighter-rouge">rtc.deine-domain.de</code> → Deine Server-IP (für LiveKit, optional)</li>
  <li><code class="language-plaintext highlighter-rouge">jwt.deine-domain.de</code> → Deine Server-IP (für JWT-Service, optional)</li>
</ul>

<p><strong>Wichtig</strong>: Stelle sicher, dass diese DNS-Einträge aktiv sind, bevor du mit dem Setup beginnst, da Let’s Encrypt die Domain-Validierung durchführt.</p>

<h2 id="schritt-1-projekt-verzeichnis-erstellen">Schritt 1: Projekt-Verzeichnis erstellen</h2>

<p>Erstelle ein neues Verzeichnis für dein Matrix-Setup:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>matrix-server
<span class="nb">cd </span>matrix-server
</code></pre></div></div>

<h2 id="schritt-2-umgebungsvariablen-konfigurieren">Schritt 2: Umgebungsvariablen konfigurieren</h2>

<p>Erstelle eine <code class="language-plaintext highlighter-rouge">.env</code> Datei mit allen notwendigen Konfigurationswerten:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano .env
</code></pre></div></div>

<p>Füge folgende Variablen ein und passe sie an deine Umgebung an:</p>

<pre><code class="language-env"># Synapse Konfiguration
SYNAPSE_SERVER_NAME=matrix.deine-domain.de
SYNAPSE_REPORT_STATS=true

# PostgreSQL
POSTGRES_PASSWORD=dein_sicheres_datenbank_passwort

# AWS S3 für Media Storage (optional, aber empfohlen)
AWS_ACCESS_KEY_ID=dein_aws_access_key
AWS_SECRET_ACCESS_KEY=dein_aws_secret_key
AWS_DEFAULT_REGION=eu-central-1
AWS_S3_ADDRESSING_STYLE=path

# TURN Server Konfiguration
TURN_DOMAIN=rtc.deine-domain.de
TURN_REALM=rtc.deine-domain.de
TURN_STATIC_AUTH_SECRET=generiere_ein_langes_zufaelliges_secret
EXTERNAL_IP=deine_oeffentliche_ip_adresse
RELAY_IP=deine_oeffentliche_ip_adresse

# LiveKit Konfiguration
LIVEKIT_API_KEY=generiere_einen_api_key
LIVEKIT_API_SECRET=generiere_ein_langes_secret

# Mautrix WhatsApp Bridge (optional)
MW_POSTGRES_PASSWORD=dein_whatsapp_bridge_datenbank_passwort
</code></pre>

<h3 id="wichtige-konfigurationsoptionen-im-detail">Wichtige Konfigurationsoptionen im Detail</h3>

<p><strong>SYNAPSE_SERVER_NAME</strong>:</p>
<ul>
  <li>Dies ist der Domain-Name deines Matrix-Servers</li>
  <li>Muss mit deiner DNS-Konfiguration übereinstimmen</li>
  <li>Beispiel: <code class="language-plaintext highlighter-rouge">matrix.example.com</code></li>
</ul>

<p><strong>SYNAPSE_REPORT_STATS</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">true</code>: Sende anonyme Statistiken an matrix.org</li>
  <li><code class="language-plaintext highlighter-rouge">false</code>: Keine Statistiken senden</li>
</ul>

<p><strong>POSTGRES_PASSWORD</strong>:</p>
<ul>
  <li>Starkes Passwort für die PostgreSQL-Datenbank</li>
  <li>Verwende einen Passwort-Generator für Production</li>
</ul>

<p><strong>TURN_STATIC_AUTH_SECRET</strong>:</p>
<ul>
  <li>Generiere ein zufälliges Secret für TURN-Authentifizierung</li>
  <li>Mindestens 32 Zeichen lang</li>
  <li>Generieren mit: <code class="language-plaintext highlighter-rouge">openssl rand -hex 32</code></li>
</ul>

<p><strong>LIVEKIT_API_KEY und LIVEKIT_API_SECRET</strong>:</p>
<ul>
  <li>Für Video-Calls benötigt</li>
  <li>Generiere mit: <code class="language-plaintext highlighter-rouge">openssl rand -hex 16</code> (für Key) und <code class="language-plaintext highlighter-rouge">openssl rand -hex 32</code> (für Secret)</li>
</ul>

<p><strong>Sicherheitshinweis</strong>: Bewahre deine <code class="language-plaintext highlighter-rouge">.env</code> Datei sicher auf und teile sie niemals öffentlich!</p>

<h2 id="schritt-3-verzeichnisstruktur-erstellen">Schritt 3: Verzeichnisstruktur erstellen</h2>

<p>Erstelle die notwendigen Verzeichnisse für Konfigurationen und Daten:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> synapse element/config.json traefik well-known/matrix/server whatsapp certs
</code></pre></div></div>

<h2 id="schritt-4-traefik-acme-datei-erstellen">Schritt 4: Traefik ACME-Datei erstellen</h2>

<p>Erstelle die Datei für Let’s Encrypt-Zertifikate:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>traefik/acme.json
<span class="nb">chmod </span>600 traefik/acme.json
</code></pre></div></div>

<p>Die Berechtigung <code class="language-plaintext highlighter-rouge">600</code> ist wichtig, damit nur der Besitzer die Datei lesen/schreiben kann.</p>

<h2 id="schritt-5-synapse-konfiguration-generieren">Schritt 5: Synapse-Konfiguration generieren</h2>

<p>Synapse benötigt eine initiale Konfigurationsdatei. Wir generieren sie mit dem offiziellen Synapse-Image:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>/synapse:/data <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">SYNAPSE_SERVER_NAME</span><span class="o">=</span>matrix.deine-domain.de <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">SYNAPSE_REPORT_STATS</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  matrixdotorg/synapse:latest generate
</code></pre></div></div>

<p><strong>Wichtig</strong>: Ersetze <code class="language-plaintext highlighter-rouge">matrix.deine-domain.de</code> mit deinem tatsächlichen Domain-Namen!</p>

<p>Dies erstellt die Datei <code class="language-plaintext highlighter-rouge">synapse/homeserver.yaml</code>. Wir werden diese später anpassen.</p>

<h2 id="schritt-6-synapse-konfiguration-anpassen">Schritt 6: Synapse-Konfiguration anpassen</h2>

<p>Öffne die generierte <code class="language-plaintext highlighter-rouge">synapse/homeserver.yaml</code> und passe folgende Einstellungen an:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano synapse/homeserver.yaml
</code></pre></div></div>

<h3 id="wichtige-konfigurationen">Wichtige Konfigurationen:</h3>

<p><strong>Datenbank-Verbindung</strong> (bereits konfiguriert durch Umgebungsvariablen):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">database</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">psycopg2</span>
  <span class="na">args</span><span class="pi">:</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s">synapse</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">${POSTGRES_PASSWORD}</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">synapse</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">db</span>
    <span class="na">cp_min</span><span class="pi">:</span> <span class="m">5</span>
    <span class="na">cp_max</span><span class="pi">:</span> <span class="m">10</span>
</code></pre></div></div>

<p><strong>Media Storage</strong> (für S3, optional):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">media_store_path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/data/media_store"</span>
<span class="na">s3</span><span class="pi">:</span>
  <span class="na">enabled</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">bucket_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">dein-s3-bucket-name"</span>
  <span class="na">region_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">eu-central-1"</span>
  <span class="na">access_key_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${AWS_ACCESS_KEY_ID}"</span>
  <span class="na">secret_access_key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${AWS_SECRET_ACCESS_KEY}"</span>
  <span class="na">addressing_style</span><span class="pi">:</span> <span class="s2">"</span><span class="s">path"</span>
</code></pre></div></div>

<p><strong>TURN-Server Konfiguration</strong>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">turn_uris</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">turn:rtc.deine-domain.de:3478?transport=udp"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">turn:rtc.deine-domain.de:3478?transport=tcp"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">turns:rtc.deine-domain.de:5349?transport=udp"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">turns:rtc.deine-domain.de:5349?transport=tcp"</span>

<span class="na">turn_shared_secret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${TURN_STATIC_AUTH_SECRET}"</span>
<span class="na">turn_user_lifetime</span><span class="pi">:</span> <span class="m">86400000</span>
<span class="na">turn_allow_guests</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p><strong>LiveKit Integration</strong> (für moderne Video-Calls):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livekit</span><span class="pi">:</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">wss://rtc.deine-domain.de/livekit/sfu"</span>
  <span class="na">api_key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${LIVEKIT_API_KEY}"</span>
  <span class="na">api_secret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${LIVEKIT_API_SECRET}"</span>
</code></pre></div></div>

<p><strong>Federation</strong> (wichtig für Kommunikation mit anderen Servern):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">federation_domain_whitelist</span><span class="pi">:</span> <span class="pi">[]</span>
<span class="c1"># Leer lassen = alle Domains erlauben</span>
<span class="c1"># Oder spezifische Domains auflisten</span>
</code></pre></div></div>

<p><strong>Registrierung</strong> (für neue Benutzer):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">enable_registration</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">enable_registration_without_verification</span><span class="pi">:</span> <span class="kc">false</span>
<span class="na">registration_shared_secret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">generiere_ein_langes_secret_hier"</span>
</code></pre></div></div>

<h2 id="schritt-7-element-konfiguration-erstellen">Schritt 7: Element-Konfiguration erstellen</h2>

<p>Erstelle die Konfigurationsdatei für Element Web:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano element/config.json
</code></pre></div></div>

<p>Füge folgende Konfiguration ein:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"default_server_config"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"m.homeserver"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"base_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://matrix.deine-domain.de"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"server_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"matrix.deine-domain.de"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"m.identity_server"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"base_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://vector.im"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"default_server_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"matrix.deine-domain.de"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"brand"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dein Chat"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"integrations_ui_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://scalar.vector.im/"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"integrations_rest_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://scalar.vector.im/api"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"integrations_widgets_urls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"https://scalar.vector.im/_matrix/integrations/v1"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"https://scalar.vector.im/_matrix/integrations/v2"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"https://scalar-staging.vector.im/_matrix/integrations/v1"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"https://scalar-staging.vector.im/_matrix/integrations/v2"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"https://scalar-staging.riot.im/scalar/api"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"bug_report_endpoint_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://element.io/bugreports/submit"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"defaultCountryCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DE"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"showLabsSettings"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"features"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"feature_new_spinner"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_pinning"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_custom_status"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_custom_tags"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_state_resolver"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_mjolnir"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_dnd"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_bridge_state"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_presence_in_room_list"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_cross_signing"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_new_device_manager"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_video_rooms"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_element_call"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"feature_livekit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"default_federate"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"default_theme"</span><span class="p">:</span><span class="w"> </span><span class="s2">"light"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"roomDirectory"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"servers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"matrix.deine-domain.de"</span><span class="p">,</span><span class="w">
      </span><span class="s2">"matrix.org"</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"settingDefaults"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"breadcrumbs"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"jitsi"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"preferredDomain"</span><span class="p">:</span><span class="w"> </span><span class="s2">"meet.jit.si"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"livekit"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"wss://rtc.deine-domain.de/livekit/sfu"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"jwt_service_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://jwt.deine-domain.de"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Wichtig</strong>: Ersetze alle <code class="language-plaintext highlighter-rouge">deine-domain.de</code> Einträge mit deiner tatsächlichen Domain!</p>

<h2 id="schritt-8-well-known-konfiguration">Schritt 8: Well-Known Konfiguration</h2>

<p>Erstelle die Well-Known-Dateien für Matrix-Discovery:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> well-known/matrix
</code></pre></div></div>

<p>Erstelle <code class="language-plaintext highlighter-rouge">well-known/matrix/server</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano well-known/matrix/server
</code></pre></div></div>

<p>Füge folgendes ein (ersetze mit deiner Domain):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"m.server"</span><span class="p">:</span><span class="w"> </span><span class="s2">"matrix.deine-domain.de:443"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Erstelle <code class="language-plaintext highlighter-rouge">well-known/matrix/client</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano well-known/matrix/client
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"m.homeserver"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"base_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://matrix.deine-domain.de"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"m.identity_server"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"base_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://vector.im"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="schritt-9-docker-compose-datei-erstellen">Schritt 9: Docker Compose Datei erstellen</h2>

<p>Erstelle die <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Datei:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano docker-compose.yml
</code></pre></div></div>

<p>Füge die vollständige docker-compose.yml ein (siehe unten im Dokument).</p>

<h2 id="schritt-10-container-starten">Schritt 10: Container starten</h2>

<p>Starte alle Container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose up <span class="nt">-d</span>
</code></pre></div></div>

<p>Der <code class="language-plaintext highlighter-rouge">-d</code> Flag startet die Container im Hintergrund. Du kannst den Status überprüfen mit:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose ps
</code></pre></div></div>

<p>Die Container sollten jetzt starten. Der erste Start kann einige Minuten dauern, da:</p>
<ul>
  <li>PostgreSQL die Datenbank initialisiert</li>
  <li>Synapse die Datenbank-Schema erstellt</li>
  <li>Traefik SSL-Zertifikate von Let’s Encrypt anfordert</li>
</ul>

<h2 id="schritt-11-logs-überwachen">Schritt 11: Logs überwachen</h2>

<p>Überwache die Logs, um sicherzustellen, dass alles korrekt startet:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Alle Container</span>
docker compose logs <span class="nt">-f</span>

<span class="c"># Nur Synapse</span>
docker compose logs <span class="nt">-f</span> synapse

<span class="c"># Nur Traefik (für SSL-Status)</span>
docker compose logs <span class="nt">-f</span> traefik
</code></pre></div></div>

<h3 id="wichtige-log-meldungen">Wichtige Log-Meldungen</h3>

<p><strong>Synapse</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Server started, listening on port 8008</code> → Server läuft</li>
  <li><code class="language-plaintext highlighter-rouge">Database is ready</code> → Datenbankverbindung erfolgreich</li>
  <li><code class="language-plaintext highlighter-rouge">Federation is ready</code> → Federation aktiviert</li>
</ul>

<p><strong>Traefik</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Certificate obtained from ACME</code> → SSL-Zertifikat erfolgreich</li>
  <li><code class="language-plaintext highlighter-rouge">Server configuration reloaded</code> → Konfiguration geladen</li>
</ul>

<h2 id="schritt-12-ersten-benutzer-erstellen">Schritt 12: Ersten Benutzer erstellen</h2>

<p>Nachdem Synapse gestartet ist, erstelle den ersten Admin-Benutzer:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nb">exec </span>synapse register_new_matrix_user <span class="nt">-c</span> /data/homeserver.yaml <span class="nt">-a</span> <span class="nt">-u</span> admin <span class="nt">-p</span> dein_admin_passwort
</code></pre></div></div>

<p><strong>Wichtig</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">-a</code> macht den Benutzer zum Administrator</li>
  <li>Verwende ein starkes Passwort!</li>
  <li>Speichere die Credentials sicher!</li>
</ul>

<h2 id="schritt-13-auf-den-server-zugreifen">Schritt 13: Auf den Server zugreifen</h2>

<p>Nach erfolgreichem Start kannst du auf folgende URLs zugreifen:</p>

<ul>
  <li><strong>Element Web Client</strong>: <code class="language-plaintext highlighter-rouge">https://chat.deine-domain.de</code></li>
  <li><strong>Synapse API</strong>: <code class="language-plaintext highlighter-rouge">https://matrix.deine-domain.de</code></li>
  <li><strong>Synapse Admin</strong>: <code class="language-plaintext highlighter-rouge">http://deine-server-ip:8081</code></li>
  <li><strong>Traefik Dashboard</strong>: <code class="language-plaintext highlighter-rouge">http://deine-server-ip:8080</code> (falls aktiviert)</li>
</ul>

<h2 id="schritt-14-erste-anmeldung">Schritt 14: Erste Anmeldung</h2>

<ol>
  <li>Öffne <code class="language-plaintext highlighter-rouge">https://chat.deine-domain.de</code> in deinem Browser</li>
  <li>Klicke auf “Sign In”</li>
  <li>Wähle “Edit” neben dem Server-Namen</li>
  <li>Gib <code class="language-plaintext highlighter-rouge">https://matrix.deine-domain.de</code> ein</li>
  <li>Melde dich mit deinem Admin-Account an</li>
</ol>

<h2 id="erweiterte-konfiguration">Erweiterte Konfiguration</h2>

<h3 id="media-storage-mit-s3">Media Storage mit S3</h3>

<p>Für Production-Umgebungen wird empfohlen, Media-Dateien in S3 zu speichern:</p>

<ol>
  <li>Erstelle einen S3-Bucket bei AWS (oder kompatiblem Service)</li>
  <li>Konfiguriere die AWS-Credentials in der <code class="language-plaintext highlighter-rouge">.env</code> Datei</li>
  <li>Aktiviere S3 in der <code class="language-plaintext highlighter-rouge">homeserver.yaml</code> (siehe Schritt 6)</li>
</ol>

<h3 id="whatsapp-bridge-einrichten">WhatsApp-Bridge einrichten</h3>

<p>Die Mautrix WhatsApp Bridge ermöglicht es, WhatsApp-Nachrichten über Matrix zu empfangen/senden:</p>

<ol>
  <li>Starte die Bridge: <code class="language-plaintext highlighter-rouge">docker compose up -d mautrix-whatsapp</code></li>
  <li>Öffne <code class="language-plaintext highlighter-rouge">http://deine-server-ip:29318</code> im Browser</li>
  <li>Folge der Anleitung zum QR-Code-Scan</li>
  <li>Die Bridge wird automatisch als AppService in Synapse registriert</li>
</ol>

<h3 id="backup-strategie">Backup-Strategie</h3>

<p><strong>Datenbank-Backup</strong>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nb">exec </span>db pg_dump <span class="nt">-U</span> synapse synapse <span class="o">&gt;</span> backup_<span class="si">$(</span><span class="nb">date</span> +%Y%m%d<span class="si">)</span>.sql
</code></pre></div></div>

<p><strong>Synapse-Daten-Backup</strong>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">tar</span> <span class="nt">-czf</span> synapse_backup_<span class="si">$(</span><span class="nb">date</span> +%Y%m%d<span class="si">)</span>.tar.gz synapse/
</code></pre></div></div>

<p><strong>Automatische Backups</strong> (mit Cron):</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Füge zu crontab hinzu (crontab -e)</span>
0 2 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="nb">cd</span> /pfad/zum/matrix-server <span class="o">&amp;&amp;</span> docker compose <span class="nb">exec</span> <span class="nt">-T</span> db pg_dump <span class="nt">-U</span> synapse synapse <span class="o">&gt;</span> backups/db_<span class="si">$(</span><span class="nb">date</span> +<span class="se">\%</span>Y<span class="se">\%</span>m<span class="se">\%</span>d<span class="si">)</span>.sql
</code></pre></div></div>

<h2 id="häufige-probleme-und-lösungen">Häufige Probleme und Lösungen</h2>

<h3 id="problem-ssl-zertifikat-wird-nicht-erstellt">Problem: SSL-Zertifikat wird nicht erstellt</h3>

<p><strong>Symptome</strong>: Traefik-Logs zeigen ACME-Fehler</p>

<p><strong>Lösungen</strong>:</p>
<ul>
  <li>Überprüfe, ob die DNS-Einträge korrekt sind: <code class="language-plaintext highlighter-rouge">dig matrix.deine-domain.de</code></li>
  <li>Stelle sicher, dass Port 80 und 443 von außen erreichbar sind</li>
  <li>Überprüfe die Firewall-Einstellungen</li>
  <li>Warte einige Minuten, Let’s Encrypt hat Rate-Limits</li>
</ul>

<h3 id="problem-synapse-startet-nicht">Problem: Synapse startet nicht</h3>

<p><strong>Symptome</strong>: Container stoppt sofort oder Logs zeigen Fehler</p>

<p><strong>Lösungen</strong>:</p>
<ul>
  <li>Überprüfe die Logs: <code class="language-plaintext highlighter-rouge">docker compose logs synapse</code></li>
  <li>Stelle sicher, dass PostgreSQL läuft: <code class="language-plaintext highlighter-rouge">docker compose ps db</code></li>
  <li>Überprüfe die <code class="language-plaintext highlighter-rouge">homeserver.yaml</code> auf Syntax-Fehler</li>
  <li>Stelle sicher, dass die Datenbank-Credentials korrekt sind</li>
</ul>

<h3 id="problem-federation-funktioniert-nicht">Problem: Federation funktioniert nicht</h3>

<p><strong>Symptome</strong>: Kann nicht mit anderen Matrix-Servern kommunizieren</p>

<p><strong>Lösungen</strong>:</p>
<ul>
  <li>Überprüfe, ob Port 8448 von außen erreichbar ist</li>
  <li>Teste mit: <code class="language-plaintext highlighter-rouge">https://federationtester.matrix.org/#matrix.deine-domain.de</code></li>
  <li>Stelle sicher, dass die Well-Known-Dateien korrekt sind</li>
  <li>Überprüfe die Traefik-Labels für Federation</li>
</ul>

<h3 id="problem-turn-server-funktioniert-nicht">Problem: TURN-Server funktioniert nicht</h3>

<p><strong>Symptome</strong>: Voice/Video-Calls funktionieren nicht</p>

<p><strong>Lösungen</strong>:</p>
<ul>
  <li>Überprüfe die TURN-Konfiguration in <code class="language-plaintext highlighter-rouge">homeserver.yaml</code></li>
  <li>Stelle sicher, dass die Ports 3478 und 5349 geöffnet sind</li>
  <li>Überprüfe die <code class="language-plaintext highlighter-rouge">TURN_STATIC_AUTH_SECRET</code> in der <code class="language-plaintext highlighter-rouge">.env</code></li>
  <li>Teste mit: <code class="language-plaintext highlighter-rouge">https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/</code></li>
</ul>

<h3 id="problem-element-lädt-nicht">Problem: Element lädt nicht</h3>

<p><strong>Symptome</strong>: Weißer Bildschirm oder Fehler beim Laden</p>

<p><strong>Lösungen</strong>:</p>
<ul>
  <li>Überprüfe die <code class="language-plaintext highlighter-rouge">config.json</code> auf Syntax-Fehler</li>
  <li>Stelle sicher, dass die Domain in der Konfiguration korrekt ist</li>
  <li>Überprüfe die Browser-Konsole auf Fehler</li>
  <li>Stelle sicher, dass Traefik die Route korrekt konfiguriert hat</li>
</ul>

<h2 id="performance-optimierung">Performance-Optimierung</h2>

<h3 id="datenbank-optimierung">Datenbank-Optimierung</h3>

<p>Füge in <code class="language-plaintext highlighter-rouge">homeserver.yaml</code> hinzu:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">database</span><span class="pi">:</span>
  <span class="na">args</span><span class="pi">:</span>
    <span class="na">cp_min</span><span class="pi">:</span> <span class="m">5</span>
    <span class="na">cp_max</span><span class="pi">:</span> <span class="m">10</span>
    <span class="na">cp_reconnect</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<h3 id="synapse-optimierung">Synapse-Optimierung</h3>

<p>Für größere Installationen:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">media_store_path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/data/media_store"</span>
<span class="na">max_upload_size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">50M"</span>
<span class="na">max_image_pixels</span><span class="pi">:</span> <span class="s2">"</span><span class="s">32M"</span>
</code></pre></div></div>

<h3 id="ressourcen-limits">Ressourcen-Limits</h3>

<p>Füge in <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Ressourcen-Limits hinzu:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">synapse</span><span class="pi">:</span>
  <span class="na">deploy</span><span class="pi">:</span>
    <span class="na">resources</span><span class="pi">:</span>
      <span class="na">limits</span><span class="pi">:</span>
        <span class="na">cpus</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2'</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s">2G</span>
      <span class="na">reservations</span><span class="pi">:</span>
        <span class="na">cpus</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1'</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s">1G</span>
</code></pre></div></div>

<h2 id="sicherheits-best-practices">Sicherheits-Best-Practices</h2>

<ol>
  <li><strong>Firewall konfigurieren</strong>:
    <ul>
      <li>Öffne nur Ports 80, 443, 8448</li>
      <li>Blockiere alle anderen Ports</li>
    </ul>
  </li>
  <li><strong>Regelmäßige Updates</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose pull
docker compose up <span class="nt">-d</span>
</code></pre></div>    </div>
  </li>
  <li><strong>Starke Passwörter</strong>:
    <ul>
      <li>Verwende Passwort-Manager</li>
      <li>Aktiviere 2FA für Admin-Accounts</li>
    </ul>
  </li>
  <li><strong>Backups</strong>:
    <ul>
      <li>Tägliche Datenbank-Backups</li>
      <li>Wöchentliche Voll-Backups</li>
      <li>Teste die Wiederherstellung regelmäßig</li>
    </ul>
  </li>
  <li><strong>Monitoring</strong>:
    <ul>
      <li>Überwache Container-Logs</li>
      <li>Setze Alerts für kritische Fehler</li>
      <li>Überwache Ressourcen-Nutzung</li>
    </ul>
  </li>
</ol>

<h2 id="nächste-schritte">Nächste Schritte</h2>

<p>Nach erfolgreicher Installation kannst du:</p>

<ol>
  <li><strong>Weitere Benutzer einladen</strong>: Erstelle Accounts für deine Community</li>
  <li><strong>Räume erstellen</strong>: Organisiere deine Kommunikation in Räumen</li>
  <li><strong>Bridges einrichten</strong>: Verbinde mit anderen Chat-Services</li>
  <li><strong>Bots hinzufügen</strong>: Automatisiere Aufgaben mit Matrix-Bots</li>
  <li><strong>Custom Branding</strong>: Passe Element an dein Branding an</li>
</ol>

<h2 id="fazit">Fazit</h2>

<p>Mit diesem Setup hast du einen vollständig funktionsfähigen Matrix-Server mit:</p>
<ul>
  <li>✅ Sichere, verschlüsselte Kommunikation</li>
  <li>✅ Voice- und Video-Calls</li>
  <li>✅ Federation mit anderen Servern</li>
  <li>✅ Automatisches SSL</li>
  <li>✅ Moderne Web-Client</li>
  <li>✅ Admin-Interface</li>
</ul>

<p>Für weitere Informationen und fortgeschrittene Konfigurationen schaue in die <a href="https://matrix-org.github.io/synapse/latest/">offizielle Synapse-Dokumentation</a>.</p>

<p>Viel Erfolg mit deinem Matrix-Server! 💬🔐</p>

<hr />

<h2 id="docker-compose-konfiguration">Docker Compose Konfiguration</h2>

<p>Hier ist die vollständige <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Datei:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik:v3.0</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="c1"># Providers</span>
      <span class="pi">-</span> <span class="s">--providers.docker=true</span>
      <span class="pi">-</span> <span class="s">--providers.docker.exposedbydefault=false</span>

      <span class="c1"># Entrypoints</span>
      <span class="pi">-</span> <span class="s">--entrypoints.web.address=:80</span>
      <span class="pi">-</span> <span class="s">--entrypoints.websecure.address=:443</span>
      <span class="pi">-</span> <span class="s">--entrypoints.federation.address=:8448</span>
      <span class="c1">#- --entrypoints.turn-udp.address=:3478/udp</span>
      <span class="c1">#- --entrypoints.turn-tcp.address=:3478/tcp</span>
      <span class="c1">#- --entrypoints.turns-udp.address=:5349/udp</span>
      <span class="c1">#- --entrypoints.turns-tcp.address=:5349/tcp</span>

      <span class="c1"># HTTP -&gt; HTTPS redirect</span>
      <span class="pi">-</span> <span class="s">--entrypoints.web.http.redirections.entrypoint.to=websecure</span>
      <span class="pi">-</span> <span class="s">--entrypoints.web.http.redirections.entrypoint.scheme=https</span>

      <span class="c1"># ACME/Let's Encrypt</span>
      <span class="pi">-</span> <span class="s">--certificatesresolvers.le.acme.tlschallenge=true</span>
      <span class="pi">-</span> <span class="s">--certificatesresolvers.le.acme.email=deine-email@deine-domain.de</span>
      <span class="pi">-</span> <span class="s">--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json</span>

      <span class="c1"># Optional: set to DEBUG if you need ACME diagnostics</span>
      <span class="c1"># - --log.level=DEBUG</span>

      <span class="c1"># Optional: dashboard on internal socket only (no port published)</span>
      <span class="pi">-</span> <span class="s">--api.dashboard=true</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">80:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">443:443"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8448:8448"</span>
      <span class="c1">#- "3478:3478/udp"</span>
      <span class="c1">#- "3478:3478/tcp"</span>
      <span class="c1">#- "5349:5349/udp"</span>
      <span class="c1">#- "5349:5349/tcp"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock:ro</span>
      <span class="pi">-</span> <span class="s">./traefik/acme.json:/letsencrypt/acme.json</span>

  <span class="na">db</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:16</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">synapse</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">${POSTGRES_PASSWORD}</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">synapse</span>
      <span class="c1"># Ensure C collation for Synapse</span>
      <span class="na">POSTGRES_INITDB_ARGS</span><span class="pi">:</span> <span class="s2">"</span><span class="s">--encoding=UTF8</span><span class="nv"> </span><span class="s">--lc-collate=C</span><span class="nv"> </span><span class="s">--lc-ctype=C"</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pg_isready</span><span class="nv"> </span><span class="s">-U</span><span class="nv"> </span><span class="s">synapse"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">10</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">dbdata:/var/lib/postgresql/data</span>

  <span class="na">synapse</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">matrixdotorg/synapse:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="na">db</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_healthy</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="c1"># Keep this aligned with homeserver.yaml server_name</span>
      <span class="na">SYNAPSE_SERVER_NAME</span><span class="pi">:</span> <span class="s">${SYNAPSE_SERVER_NAME}</span>
      <span class="na">SYNAPSE_REPORT_STATS</span><span class="pi">:</span> <span class="s">${SYNAPSE_REPORT_STATS}</span>
      <span class="na">AWS_ACCESS_KEY_ID</span><span class="pi">:</span> <span class="s">${AWS_ACCESS_KEY_ID}</span>
      <span class="na">AWS_SECRET_ACCESS_KEY</span><span class="pi">:</span> <span class="s">${AWS_SECRET_ACCESS_KEY}</span>
      <span class="na">AWS_DEFAULT_REGION</span><span class="pi">:</span> <span class="s">${AWS_DEFAULT_REGION}</span>
      <span class="na">AWS_S3_ADDRESSING_STYLE</span><span class="pi">:</span> <span class="s">${AWS_S3_ADDRESSING_STYLE}</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./synapse:/data</span>
      <span class="pi">-</span> <span class="s">./whatsapp/:/whatsapp/</span>
      <span class="c1">#- mw_data:/data</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8008:8008"</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>

      <span class="c1"># Client API via 443</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-web.rule=Host(`matrix.deine-domain.de`) &amp;&amp; (PathPrefix(`/_matrix`) || PathPrefix(`/_synapse`))</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-web.entrypoints=websecure</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-web.tls.certresolver=le</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-web.service=synapse-web</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.synapse-web.loadbalancer.server.port=8008</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.synapse-buf.buffering.maxRequestBodyBytes=0</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-web.middlewares=synapse-buf</span>

      <span class="c1"># Federation via 8448</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-fed.rule=Host(`matrix.deine-domain.de`) &amp;&amp; PathPrefix(`/_matrix`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-fed.entrypoints=federation</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-fed.tls.certresolver=le</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.synapse-fed.service=synapse-fed</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.synapse-fed.loadbalancer.server.port=8008</span>

  <span class="na">element</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">vectorim/element-web:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">synapse</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="c1"># Provide your Element config</span>
      <span class="pi">-</span> <span class="s">./element/config.json:/app/config.json:ro</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.element.rule=Host(`chat.deine-domain.de`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.element.entrypoints=websecure</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.element.tls.certresolver=le</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.element.service=element</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.element.loadbalancer.server.port=80</span>

  <span class="na">admin</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">etkecc/synapse-admin</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">admin</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">REACT_APP_SERVER</span><span class="pi">:</span> <span class="s">https://matrix.deine-domain.de</span>
      <span class="na">TZ</span><span class="pi">:</span> <span class="s">Europe/Berlin</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8081:80"</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>

  <span class="na">certs-dumper</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ldez/traefik-certs-dumper:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./traefik/acme.json:/acme.json:ro</span>
      <span class="pi">-</span> <span class="s">./certs:/out</span>
    <span class="na">env_file</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">.env</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="c1"># Export-Modus: directory mit fullchain.pem und privkey.pem pro Domain</span>
      <span class="pi">-</span> <span class="s">DOMAIN=${TURN_DOMAIN}</span>
    <span class="na">entrypoint</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">sh</span>
      <span class="pi">-</span> <span class="s">-c</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">set -e</span>
        <span class="s">while true; do</span>
          <span class="s">traefik-certs-dumper file --version v2 --watch=false --source /acme.json --domain-subdir --dest /out</span>
          <span class="s">if [ -f "${CERTS_PATH}/certificate.crt" ]; then</span>
            <span class="s">chown 0:65534 "${CERTS_PATH}/certificate.crt" || true</span>
            <span class="s">chmod 0640 "${CERTS_PATH}/certificate.crt" || true</span>
          <span class="s">fi</span>
          <span class="s">if [ -f "${CERTS_PATH}/privatekey.key" ]; then</span>
            <span class="s">chown 0:65534 "${CERTS_PATH}/privatekey.key" || true</span>
            <span class="s">chmod 0640 "${CERTS_PATH}/privatekey.key" || true</span>
          <span class="s">fi</span>
          <span class="s">sleep 3600</span>
        <span class="s">done</span>

  <span class="na">coturn</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">coturn/coturn:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">coturn</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">certs-dumper</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s2">"</span><span class="s">root:root"</span>
    <span class="na">entrypoint</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">sh</span>
      <span class="pi">-</span> <span class="s">-c</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s">set -e</span>
        <span class="s">#echo "Waiting for $${CERTS_PATH}/certificate.crt and $${CERTS_PATH}/privatekey.key ..."</span>
        <span class="s">#for i in $$(seq 1 180); do</span>
        <span class="s">#  [ -f "$${CERTS_PATH}/certificate.crt" ] &amp;&amp; [ -f "$${CERTS_PATH}/privatekey.key" ] &amp;&amp; break</span>
        <span class="s">#  sleep 1</span>
        <span class="s">#done</span>
        <span class="s">#[ -f "$${CERTS_PATH}/certificate.crt" ] &amp;&amp; [ -f "$${CERTS_PATH}/privatekey.key" ] || { echo "Certs missing after 180s"; exit 1; }</span>

        <span class="s">exec turnserver \</span>
          <span class="s">--log-file=stdout \</span>
          <span class="s">--no-cli \</span>
          <span class="s">--fingerprint \</span>
          <span class="s">--listening-port=3478 \</span>
          <span class="s">--tls-listening-port=5349 \</span>
          <span class="s">--use-auth-secret \</span>
          <span class="s">--static-auth-secret="$${TURN_STATIC_AUTH_SECRET}" \</span>
          <span class="s">--realm="$${TURN_REALM}" \</span>
          <span class="s">--min-port=49160 \</span>
          <span class="s">--max-port=49200 \</span>
          <span class="s">--cert="$${CERTS_PATH}/certificate.crt" \</span>
          <span class="s">--pkey="$${CERTS_PATH}/privatekey.key" \</span>
          <span class="s">--no-tlsv1 \</span>
          <span class="s">--stale-nonce=600 \</span>
          <span class="s">--total-quota=100 \</span>
          <span class="s">--no-multicast-peers \</span>
          <span class="s">--no-tcp-relay \</span>
          <span class="s">$${EXTERNAL_IP:+--external-ip=$${EXTERNAL_IP}} \</span>
          <span class="s">$${RELAY_IP:+--relay-ip=$${RELAY_IP}}</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">CERTS_PATH="/certs/${TURN_DOMAIN}"</span>
      <span class="pi">-</span> <span class="s">TURN_REALM=${TURN_REALM}</span>
      <span class="pi">-</span> <span class="s">TURN_STATIC_AUTH_SECRET=${TURN_STATIC_AUTH_SECRET}</span>
      <span class="pi">-</span> <span class="s">TURN_DOMAIN=${TURN_DOMAIN}</span>
      <span class="pi">-</span> <span class="s">EXTERNAL_IP=${EXTERNAL_IP}</span>
      <span class="pi">-</span> <span class="s">RELAY_IP=${RELAY_IP}</span>
    <span class="na">network_mode</span><span class="pi">:</span> <span class="s">host</span>
    <span class="c1">#ports:</span>
    <span class="c1">#  - "3478:3478/tcp"</span>
    <span class="c1">#  - "3478:3478/udp"</span>
    <span class="c1">#  - "5349:5349/tcp"</span>
    <span class="c1">#  - "5349:5349/udp"</span>
    <span class="c1">#  - "49160-49200:49160-49200/udp"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./certs:/certs:ro</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">timeout</span><span class="nv"> </span><span class="s">2</span><span class="nv"> </span><span class="s">bash</span><span class="nv"> </span><span class="s">-c</span><span class="nv"> </span><span class="s">'&lt;/dev/tcp/127.0.0.1/3478'</span><span class="nv"> </span><span class="s">||</span><span class="nv"> </span><span class="s">exit</span><span class="nv"> </span><span class="s">1"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">30s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turn-udp.rule=HostSNI(`*`)</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turn-udp.entrypoints=turn-udp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turn-udp.service=turn-udp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.services.turn-udp.loadbalancer.server.port=3478</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turn-tcp.rule=HostSNI(`*`)</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turn-tcp.entrypoints=turn-tcp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turn-tcp.service=turn-tcp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.services.turn-tcp.loadbalancer.server.port=3478</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turns-udp.rule=HostSNI(`*`)</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turns-udp.entrypoints=turns-udp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turns-udp.service=turns-udp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.services.turns-udp.loadbalancer.server.port=5349</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turns-tcp.rule=HostSNI(`*`)</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turns-tcp.entrypoints=turns-tcp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.routers.turns-tcp.service=turns-tcp</span>
      <span class="pi">-</span> <span class="s">traefik.tcp.services.turns-tcp.loadbalancer.server.port=5349</span>

  <span class="na">livekit</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">livekit/livekit-server:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">LIVEKIT_PORT</span><span class="pi">:</span> <span class="s2">"</span><span class="s">7880"</span>
      <span class="na">LIVEKIT_RTC_TCP_PORT</span><span class="pi">:</span> <span class="s2">"</span><span class="s">7881"</span>
      <span class="na">LIVEKIT_RTC_UDP_PORT</span><span class="pi">:</span> <span class="s2">"</span><span class="s">7882"</span>        <span class="c1"># alternativ Port-Range 50000-60000/udp</span>
      <span class="na">LIVEKIT_KEYS</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${LIVEKIT_API_KEY}:</span><span class="nv"> </span><span class="s">${LIVEKIT_API_SECRET}"</span>
      <span class="na">LIVEKIT_WEBRTC_USE_EXTERNAL_IP</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">--bind 0.0.0.0</span>
      <span class="s">--node-ip 0.0.0.0</span>
      <span class="s">--port 7880</span>
      <span class="s">#--rtc.tcp_port 7881</span>
      <span class="s">#--rtc.udp_port 7882</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.livekit.rule=Host(`rtc.deine-domain.de`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.livekit.entrypoints=websecure</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.livekit.tls.certresolver=le</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.livekit-strip.stripprefix.prefixes=/livekit/sfu</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.livekit.middlewares=livekit-strip</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.livekit.loadbalancer.server.port=7880</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">7881:7881/tcp"</span>     <span class="c1"># WebRTC TCP fallback</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">7882:7882/udp"</span>     <span class="c1"># WebRTC UDP (UDP-Mux)</span>
      <span class="c1"># Falls du statt UDP-Mux lieber Range nutzt: 50000-60000/udp publishen</span>

  <span class="na">lk-jwt</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/element-hq/lk-jwt-service:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">LIVEKIT_URL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">wss://rtc.deine-domain.de/livekit/sfu"</span>   <span class="c1"># via Traefik auf 7880</span>
      <span class="na">LIVEKIT_KEY</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${LIVEKIT_API_KEY}"</span>
      <span class="na">LIVEKIT_SECRET</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${LIVEKIT_API_SECRET}"</span>
      <span class="c1"># optional: RATE_LIMIT, LOG_LEVEL usw.</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="c1"># Route: Host + Pfadpräfix</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.lkjwt.rule=Host(`jwt.deine-domain.de`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.lkjwt.entrypoints=websecure</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.lkjwt.tls.certresolver=le</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.lkjwt.loadbalancer.server.port=8080</span>

      <span class="c1"># CORS freischalten (sonst blockt der Browser die JWT-Anfrage)</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.lkjwt-cors.headers.accesscontrolallowmethods=GET,OPTIONS</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.lkjwt-cors.headers.accesscontrolallowheaders=*</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.lkjwt-cors.headers.accesscontrolalloworiginlist=*</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.lkjwt-cors.headers.addvaryheader=true</span>

      <span class="c1"># Middlewares anwenden (Reihenfolge egal)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.lkjwt.middlewares=lkjwt-cors</span>

  <span class="na">wellknown</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:alpine</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./well-known:/usr/share/nginx/html/.well-known:ro</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.wellknown.rule=Host(`matrix.deine-domain.de`) &amp;&amp; PathPrefix(`/.well-known/matrix`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.wellknown.entrypoints=websecure</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.wellknown.tls.certresolver=le</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.wellknown.loadbalancer.server.port=80</span>

  <span class="na">mw-db</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:16</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">mautrix_whatsapp</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">mautrix</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">${MW_POSTGRES_PASSWORD:-change_me}</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mw_db:/var/lib/postgresql/data</span>

  <span class="na">mautrix-whatsapp</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">dock.mau.dev/mautrix/whatsapp:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mw-db</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./whatsapp/:/data/</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">29318:29318"</span>    <span class="c1"># AppService-Callback Port für Synapse</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">wget"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-qO-"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">http://localhost:29318/health"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">30s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">dbdata</span><span class="pi">:</span>
  <span class="na">mw_db</span><span class="pi">:</span>
  <span class="na">mw_data</span><span class="pi">:</span>
</code></pre></div></div>

<p><strong>Wichtig</strong>: Ersetze alle <code class="language-plaintext highlighter-rouge">deine-domain.de</code> Einträge in der docker-compose.yml mit deiner tatsächlichen Domain!</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="matrix" /><category term="chat" /><category term="security" /><summary type="html"><![CDATA[Matrix ist ein dezentrales, offenes Protokoll für sichere, Echtzeit-Kommunikation. Es ermöglicht dir, deinen eigenen Chat-Server zu betreiben, der vollständig unter deiner Kontrolle steht und mit anderen Matrix-Servern kommunizieren kann (Federation). In diesem ausführlichen Tutorial zeige ich dir, wie du einen vollständigen Matrix-Server mit Docker Compose aufsetzt, inklusive Synapse (der Referenz-Server), Element (Web-Client), Traefik (Reverse-Proxy), TURN-Server für Voice/Video und optional WhatsApp-Bridge.]]></summary></entry><entry><title type="html">Keycloak im Docker aufsetzen</title><link href="https://docs.pazdzewicz.de/keycloak/oauth/security/2025/09/10/keycloak-setup.html" rel="alternate" type="text/html" title="Keycloak im Docker aufsetzen" /><published>2025-09-10T12:00:00+00:00</published><updated>2025-09-10T12:00:00+00:00</updated><id>https://docs.pazdzewicz.de/keycloak/oauth/security/2025/09/10/keycloak-setup</id><content type="html" xml:base="https://docs.pazdzewicz.de/keycloak/oauth/security/2025/09/10/keycloak-setup.html"><![CDATA[<p>Keycloak ist ein leistungsstarkes, Open-Source Identity and Access Management System (IAM), das Single Sign-On (SSO), OAuth 2.0, OpenID Connect und viele weitere Authentifizierungs- und Autorisierungsfunktionen bietet. In diesem Tutorial zeige ich dir, wie du Keycloak mit Docker und PostgreSQL einrichtest und in Betrieb nimmst.</p>

<h2 id="was-ist-keycloak">Was ist Keycloak?</h2>

<p>Keycloak ist eine umfassende Lösung für die Verwaltung von Benutzeridentitäten und Zugriffsrechten. Es ermöglicht dir:</p>

<ul>
  <li><strong>Single Sign-On (SSO)</strong> zwischen verschiedenen Anwendungen</li>
  <li><strong>OAuth 2.0 und OpenID Connect</strong> für moderne Authentifizierungsflows</li>
  <li><strong>Social Login</strong> Integration (Google, Facebook, GitHub, etc.)</li>
  <li><strong>Benutzerverwaltung</strong> mit Self-Service-Registrierung</li>
  <li><strong>Rollen- und Rechteverwaltung</strong> (RBAC)</li>
  <li><strong>Zwei-Faktor-Authentifizierung (2FA)</strong></li>
  <li><strong>Token-Management</strong> für JWT und SAML</li>
</ul>

<h2 id="voraussetzungen">Voraussetzungen</h2>

<p>Bevor wir mit der Installation beginnen, stelle sicher, dass du folgende Tools auf deinem System installiert hast:</p>

<ul>
  <li><strong>Docker</strong> - <a href="https://docs.docker.com/get-docker/">Download</a></li>
  <li><strong>Docker Compose</strong> - <a href="https://docs.docker.com/compose/install/">Download</a></li>
</ul>

<h3 id="installation-prüfen">Installation prüfen</h3>

<p>Überprüfe, ob Docker und Docker Compose installiert sind:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nt">--version</span>
docker compose version
</code></pre></div></div>

<h2 id="schritt-1-repository-klonen">Schritt 1: Repository klonen</h2>

<p>Klone das Keycloak Docker-Setup Repository:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://gitlab.com/pazdzewicz_docker/keycloak
<span class="nb">cd </span>keycloak
</code></pre></div></div>

<p>Dieses Repository enthält eine vorkonfigurierte Docker Compose-Datei für Keycloak mit PostgreSQL.</p>

<h2 id="schritt-2-docker-images-herunterladen">Schritt 2: Docker Images herunterladen</h2>

<p>Lade die notwendigen Docker Images herunter:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose pull
</code></pre></div></div>

<p>Dieser Befehl lädt die neuesten Images für Keycloak und PostgreSQL herunter. Je nach Internetverbindung kann dies einige Minuten dauern.</p>

<h2 id="schritt-3-umgebungsvariablen-konfigurieren">Schritt 3: Umgebungsvariablen konfigurieren</h2>

<p>Erstelle eine <code class="language-plaintext highlighter-rouge">.env</code> Datei basierend auf der <code class="language-plaintext highlighter-rouge">env.template</code> Datei im Repository:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>env.template .env
</code></pre></div></div>

<p>Öffne die <code class="language-plaintext highlighter-rouge">.env</code> Datei und passe die Werte an deine Bedürfnisse an. Hier sind die wichtigsten Konfigurationsoptionen:</p>

<h3 id="umgebungsvariablen-im-detail">Umgebungsvariablen im Detail</h3>

<p>Die <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Datei verwendet folgende Umgebungsvariablen:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">environment</span><span class="pi">:</span>
  <span class="na">JAVA_OPTS_APPEND</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${JAVA_OPTS_APPEND}"</span>
  <span class="na">KC_HEALTH_ENABLED</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_HEALTH_ENABLED}"</span>
  <span class="na">KC_HTTP_ENABLED</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_HTTP_ENABLED}"</span>
  <span class="na">KC_METRICS_ENABLED</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_METRICS_ENABLED}"</span>
  <span class="na">KC_FEATURES</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_FEATURES}"</span>
  <span class="na">KC_FEATURES_DISABLED</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_FEATURES_DISABLED}"</span>
  <span class="na">KC_HOSTNAME_URL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_HOSTNAME_URL}"</span>
  <span class="na">KC_PROXY</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_PROXY}"</span>
  <span class="na">KC_DB</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_DB}"</span>
  <span class="na">KC_DB_URL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_DB_URL}"</span>
  <span class="na">KC_DB_USERNAME</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_DB_USERNAME}"</span>
  <span class="na">KC_DB_PASSWORD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KC_DB_PASSWORD}"</span>
  <span class="na">KEYCLOAK_ADMIN</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KEYCLOAK_ADMIN}"</span>
  <span class="na">KEYCLOAK_ADMIN_PASSWORD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${KEYCLOAK_ADMIN_PASSWORD}"</span>
</code></pre></div></div>

<h3 id="wichtige-konfigurationsoptionen">Wichtige Konfigurationsoptionen</h3>

<p><strong>Java-Optionen:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">JAVA_OPTS_APPEND</code>: Zusätzliche JVM-Parameter (z.B. für Memory-Einstellungen)</li>
</ul>

<p><strong>Health &amp; Monitoring:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">KC_HEALTH_ENABLED</code>: Aktiviert Health-Check-Endpunkte (Standard: <code class="language-plaintext highlighter-rouge">true</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">KC_METRICS_ENABLED</code>: Aktiviert Metriken-Endpunkte (Standard: <code class="language-plaintext highlighter-rouge">false</code>)</li>
</ul>

<p><strong>Netzwerk &amp; Proxy:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">KC_HTTP_ENABLED</code>: Aktiviert HTTP-Zugriff (Standard: <code class="language-plaintext highlighter-rouge">true</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">KC_HOSTNAME_URL</code>: Die öffentliche URL deines Keycloak-Servers</li>
  <li><code class="language-plaintext highlighter-rouge">KC_PROXY</code>: Proxy-Modus (<code class="language-plaintext highlighter-rouge">edge</code>, <code class="language-plaintext highlighter-rouge">reencrypt</code>, <code class="language-plaintext highlighter-rouge">passthrough</code>, <code class="language-plaintext highlighter-rouge">none</code>)</li>
</ul>

<p><strong>Datenbank:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">KC_DB</code>: Datenbanktyp (<code class="language-plaintext highlighter-rouge">postgres</code>, <code class="language-plaintext highlighter-rouge">mysql</code>, <code class="language-plaintext highlighter-rouge">mariadb</code>, etc.)</li>
  <li><code class="language-plaintext highlighter-rouge">KC_DB_URL</code>: JDBC-Verbindungs-URL zur Datenbank</li>
  <li><code class="language-plaintext highlighter-rouge">KC_DB_USERNAME</code>: Datenbank-Benutzername</li>
  <li><code class="language-plaintext highlighter-rouge">KC_DB_PASSWORD</code>: Datenbank-Passwort</li>
</ul>

<p><strong>Administration:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">KEYCLOAK_ADMIN</code>: Admin-Benutzername für Keycloak</li>
  <li><code class="language-plaintext highlighter-rouge">KEYCLOAK_ADMIN_PASSWORD</code>: Admin-Passwort für Keycloak</li>
</ul>

<p><strong>Features:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">KC_FEATURES</code>: Zu aktivierende Features (kommagetrennt)</li>
  <li><code class="language-plaintext highlighter-rouge">KC_FEATURES_DISABLED</code>: Zu deaktivierende Features (kommagetrennt)</li>
</ul>

<h3 id="beispiel-env-datei">Beispiel .env Datei</h3>

<pre><code class="language-env"># Java Options
JAVA_OPTS_APPEND="-Xms512m -Xmx1024m"

# Health &amp; Monitoring
KC_HEALTH_ENABLED=true
KC_METRICS_ENABLED=true

# Network
KC_HTTP_ENABLED=true
KC_HOSTNAME_URL=http://localhost:8080
KC_PROXY=edge

# Database
KC_DB=postgres
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=change_me_secure_password

# Admin Credentials
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=change_me_admin_password

# Features
KC_FEATURES=token-exchange,admin-fine-grained-authz
KC_FEATURES_DISABLED=
</code></pre>

<p><strong>Sicherheitshinweis</strong>: Ändere die Passwörter in deiner <code class="language-plaintext highlighter-rouge">.env</code> Datei! Verwende starke, eindeutige Passwörter für Production-Umgebungen.</p>

<h2 id="schritt-4-keycloak-starten">Schritt 4: Keycloak starten</h2>

<p>Starte die Keycloak-Container im Hintergrund:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose up <span class="nt">-d</span>
</code></pre></div></div>

<p>Der <code class="language-plaintext highlighter-rouge">-d</code> Flag startet die Container im Detached-Modus (im Hintergrund). Du kannst den Status der Container mit folgendem Befehl überprüfen:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose ps
</code></pre></div></div>

<p>Die Container sollten jetzt laufen. Keycloak ist standardmäßig unter <code class="language-plaintext highlighter-rouge">http://localhost:8080</code> erreichbar.</p>

<h2 id="schritt-5-erste-anmeldung">Schritt 5: Erste Anmeldung</h2>

<p>Nachdem Keycloak gestartet ist, öffne deinen Browser und navigiere zu:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost:8080
</code></pre></div></div>

<p>Du wirst zur Keycloak-Administrationskonsole weitergeleitet. Melde dich mit den in deiner <code class="language-plaintext highlighter-rouge">.env</code> Datei konfigurierten Admin-Credentials an:</p>

<ul>
  <li><strong>Benutzername</strong>: Der Wert von <code class="language-plaintext highlighter-rouge">KEYCLOAK_ADMIN</code></li>
  <li><strong>Passwort</strong>: Der Wert von <code class="language-plaintext highlighter-rouge">KEYCLOAK_ADMIN_PASSWORD</code></li>
</ul>

<h2 id="sslhttps-setup">SSL/HTTPS Setup</h2>

<p>Für Production-Umgebungen wird dringend empfohlen, HTTPS zu verwenden. Eine bewährte Methode ist, einen Reverse-Proxy wie Traefik vor Keycloak zu betreiben.</p>

<h3 id="warum-traefik">Warum Traefik?</h3>

<p>Traefik bietet:</p>
<ul>
  <li>Automatische SSL-Zertifikate mit Let’s Encrypt</li>
  <li>Reverse-Proxy-Funktionalität</li>
  <li>Load Balancing</li>
  <li>Einfache Konfiguration über Labels</li>
</ul>

<h3 id="keycloak-hinter-einem-reverse-proxy">Keycloak hinter einem Reverse-Proxy</h3>

<p>Wenn du Keycloak hinter einem Reverse-Proxy betreibst, setze <code class="language-plaintext highlighter-rouge">KC_PROXY</code> entsprechend:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">edge</code></strong>: Proxy terminiert SSL, Keycloak läuft auf HTTP (häufigste Konfiguration)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">reencrypt</code></strong>: Proxy terminiert SSL und verschlüsselt erneut zu Keycloak</li>
  <li><strong><code class="language-plaintext highlighter-rouge">passthrough</code></strong>: Proxy leitet SSL-Traffic durch</li>
  <li><strong><code class="language-plaintext highlighter-rouge">none</code></strong>: Kein Proxy (nur für Entwicklung)</li>
</ul>

<p>Stelle sicher, dass <code class="language-plaintext highlighter-rouge">KC_HOSTNAME_URL</code> auf die öffentliche HTTPS-URL zeigt.</p>

<h2 id="keycloak-administration-über-cli">Keycloak Administration über CLI</h2>

<p>Keycloak bietet umfangreiche CLI-Tools für die Administration. Du kannst in den Keycloak-Container eintreten und diese Tools verwenden.</p>

<h3 id="in-den-container-eintreten">In den Container eintreten</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nb">exec</span> <span class="nt">-it</span> keycloak bash
</code></pre></div></div>

<p>Oder direkt zum <code class="language-plaintext highlighter-rouge">_bin</code> Verzeichnis:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nb">exec</span> <span class="nt">-it</span> keycloak /bin/bash
</code></pre></div></div>

<h3 id="kcsh-verwenden">kc.sh verwenden</h3>

<p>Das <code class="language-plaintext highlighter-rouge">kc.sh</code> Skript ist das Hauptwerkzeug für die Keycloak-Administration über die CLI:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /opt/keycloak
./bin/kc.sh <span class="nt">--help</span>
</code></pre></div></div>

<h3 id="verfügbare-cli-kommandos">Verfügbare CLI-Kommandos</h3>

<p>Keycloak bietet verschiedene Kommandos für die Administration:</p>

<p><strong>Realm-Verwaltung:</strong></p>
<ul>
  <li>Realm exportieren: <code class="language-plaintext highlighter-rouge">./bin/kc.sh export --realm myrealm</code></li>
  <li>Realm importieren: <code class="language-plaintext highlighter-rouge">./bin/kc.sh import --file /path/to/realm.json</code></li>
</ul>

<p><strong>Datenbank-Verwaltung:</strong></p>
<ul>
  <li>Datenbank exportieren: Backup-Skripte im <code class="language-plaintext highlighter-rouge">_bin/</code> Verzeichnis</li>
  <li>Datenbank importieren: Restore-Skripte im <code class="language-plaintext highlighter-rouge">_bin/</code> Verzeichnis</li>
</ul>

<h3 id="nützliche-skripte-im-_bin-verzeichnis">Nützliche Skripte im _bin/ Verzeichnis</h3>

<p>Das Repository enthält verschiedene nützliche Skripte:</p>

<ul>
  <li><strong>Realm Export/Import</strong>: Automatisierte Realm-Verwaltung</li>
  <li><strong>Datenbank Backup</strong>: Regelmäßige Backups für Disaster Recovery</li>
  <li><strong>Konfigurationsmanagement</strong>: Skripte zur Konfigurationsverwaltung</li>
</ul>

<p>Diese Skripte sind besonders nützlich für:</p>

<ul>
  <li><strong>Regelmäßige Backups</strong> mit Tools wie Restic oder ähnlichen Backup-Lösungen</li>
  <li><strong>Automatisierte Deployments</strong> in CI/CD-Pipelines</li>
  <li><strong>Disaster Recovery</strong> Szenarien</li>
</ul>

<h2 id="container-logs-überwachen">Container-Logs überwachen</h2>

<p>Um die Logs von Keycloak zu überprüfen:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose logs <span class="nt">-f</span> keycloak
</code></pre></div></div>

<p>Für alle Container gleichzeitig:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose logs <span class="nt">-f</span>
</code></pre></div></div>

<h2 id="erste-schritte-mit-keycloak">Erste Schritte mit Keycloak</h2>

<p>Nach der erfolgreichen Installation kannst du:</p>

<ol>
  <li><strong>Einen Realm erstellen</strong>: Ein Realm ist ein isolierter Bereich für Benutzer, Clients und Konfigurationen</li>
  <li><strong>Clients konfigurieren</strong>: Registriere deine Anwendungen als Clients</li>
  <li><strong>Benutzer erstellen</strong>: Füge Benutzer manuell hinzu oder aktiviere Self-Registration</li>
  <li><strong>Rollen definieren</strong>: Erstelle Rollen und weise sie Benutzern zu</li>
  <li><strong>Identity Provider verbinden</strong>: Integriere Social Login oder andere Identity Provider</li>
</ol>

<h2 id="häufige-probleme-und-lösungen">Häufige Probleme und Lösungen</h2>

<h3 id="problem-container-startet-nicht">Problem: Container startet nicht</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe die Logs: <code class="language-plaintext highlighter-rouge">docker compose logs keycloak</code></li>
  <li>Stelle sicher, dass der PostgreSQL-Container läuft: <code class="language-plaintext highlighter-rouge">docker compose ps</code></li>
  <li>Überprüfe die <code class="language-plaintext highlighter-rouge">.env</code> Datei auf korrekte Werte</li>
</ul>

<h3 id="problem-datenbankverbindungsfehler">Problem: Datenbankverbindungsfehler</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe, ob PostgreSQL läuft: <code class="language-plaintext highlighter-rouge">docker compose ps postgres</code></li>
  <li>Verifiziere die Datenbank-Credentials in der <code class="language-plaintext highlighter-rouge">.env</code> Datei</li>
  <li>Stelle sicher, dass <code class="language-plaintext highlighter-rouge">KC_DB_URL</code> korrekt formatiert ist</li>
</ul>

<h3 id="problem-port-bereits-belegt">Problem: Port bereits belegt</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Finde den Prozess, der Port 8080 verwendet: <code class="language-plaintext highlighter-rouge">sudo lsof -i :8080</code></li>
  <li>Ändere den Port in der <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Datei</li>
  <li>Oder beende den konfliktierenden Prozess</li>
</ul>

<h3 id="problem-admin-login-funktioniert-nicht">Problem: Admin-Login funktioniert nicht</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Überprüfe, ob die Admin-Credentials in der <code class="language-plaintext highlighter-rouge">.env</code> Datei korrekt sind</li>
  <li>Stelle sicher, dass der Container vollständig gestartet ist</li>
  <li>Warte einige Sekunden nach dem Start, bevor du dich anmeldest</li>
</ul>

<h3 id="problem-keycloak-startet-sehr-langsam">Problem: Keycloak startet sehr langsam</h3>

<p><strong>Lösung</strong>:</p>
<ul>
  <li>Erhöhe die Java Memory-Optionen: <code class="language-plaintext highlighter-rouge">JAVA_OPTS_APPEND="-Xms1024m -Xmx2048m"</code></li>
  <li>Überprüfe die verfügbaren Systemressourcen</li>
  <li>Stelle sicher, dass die Datenbank schnell genug ist</li>
</ul>

<h2 id="best-practices">Best Practices</h2>

<ol>
  <li><strong>Sicherheit</strong>:
    <ul>
      <li>Verwende starke Passwörter für Admin- und Datenbank-Zugänge</li>
      <li>Setze Keycloak hinter einen Reverse-Proxy mit SSL</li>
      <li>Aktualisiere Keycloak regelmäßig auf die neueste Version</li>
    </ul>
  </li>
  <li><strong>Backups</strong>:
    <ul>
      <li>Führe regelmäßige Backups der Datenbank durch</li>
      <li>Exportiere wichtige Realms regelmäßig</li>
      <li>Teste die Wiederherstellung von Backups</li>
    </ul>
  </li>
  <li><strong>Monitoring</strong>:
    <ul>
      <li>Aktiviere Health-Checks: <code class="language-plaintext highlighter-rouge">KC_HEALTH_ENABLED=true</code></li>
      <li>Aktiviere Metriken: <code class="language-plaintext highlighter-rouge">KC_METRICS_ENABLED=true</code></li>
      <li>Überwache Container-Logs regelmäßig</li>
    </ul>
  </li>
  <li><strong>Performance</strong>:
    <ul>
      <li>Passe Java Memory-Optionen an deine Hardware an</li>
      <li>Verwende eine leistungsstarke Datenbank</li>
      <li>Cache-Konfiguration für Production optimieren</li>
    </ul>
  </li>
</ol>

<h2 id="nächste-schritte">Nächste Schritte</h2>

<p>Nach erfolgreicher Installation kannst du:</p>

<ol>
  <li><strong>Realms konfigurieren</strong>: Richte deine ersten Realms ein</li>
  <li><strong>Clients registrieren</strong>: Verbinde deine Anwendungen mit Keycloak</li>
  <li><strong>Social Login aktivieren</strong>: Integriere Google, GitHub oder andere Provider</li>
  <li><strong>Custom Themes erstellen</strong>: Passe das Aussehen der Login-Seiten an</li>
  <li><strong>User Federation einrichten</strong>: Verbinde externe User-Datenbanken</li>
</ol>

<h2 id="fazit">Fazit</h2>

<p>Dieses Setup bietet dir eine solide Grundlage für die Verwendung von Keycloak mit Docker und PostgreSQL. Mit dieser Konfiguration hast du eine vollständig funktionsfähige Identity-Management-Lösung, die du nach deinen Bedürfnissen anpassen kannst.</p>

<p>Für fortgeschrittene Konfigurationen und Features schaue in die <a href="https://www.keycloak.org/documentation">offizielle Keycloak-Dokumentation</a>.</p>

<p>Viel Erfolg mit deinem Keycloak-Setup! 🔐</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="keycloak" /><category term="oauth" /><category term="security" /><summary type="html"><![CDATA[Keycloak ist ein leistungsstarkes, Open-Source Identity and Access Management System (IAM), das Single Sign-On (SSO), OAuth 2.0, OpenID Connect und viele weitere Authentifizierungs- und Autorisierungsfunktionen bietet. In diesem Tutorial zeige ich dir, wie du Keycloak mit Docker und PostgreSQL einrichtest und in Betrieb nimmst.]]></summary></entry><entry><title type="html">Der perfekte Kubernetes Production Cluster mit Hetzner Dedicated Server, Proxmox VE, K3S und BGP</title><link href="https://docs.pazdzewicz.de/kubernetes/hetzner/bgp/proxmox/2024/11/13/hetzner-k8s-sdn.html" rel="alternate" type="text/html" title="Der perfekte Kubernetes Production Cluster mit Hetzner Dedicated Server, Proxmox VE, K3S und BGP" /><published>2024-11-13T10:12:00+00:00</published><updated>2024-11-13T10:12:00+00:00</updated><id>https://docs.pazdzewicz.de/kubernetes/hetzner/bgp/proxmox/2024/11/13/hetzner-k8s-sdn</id><content type="html" xml:base="https://docs.pazdzewicz.de/kubernetes/hetzner/bgp/proxmox/2024/11/13/hetzner-k8s-sdn.html"><![CDATA[<h1 id="einleitung">Einleitung</h1>

<p>Wie wir bereits im Artikel <a href="/kubernetes/2023/12/10/kubernetes-pricing.html">Kubernetes Preisvergleich</a> festgestellt haben, bietet Hetzner ein exzellentes Preis-Leistungs-Verhältnis für Kubernetes-Cluster. Die Kombination aus leistungsfähiger Hardware und attraktiven Preisen macht Hetzner zu einer idealen Wahl für produktive Umgebungen.</p>

<p>In diesem Beitrag zeigen wir euch, wie wir auf den Dedicated Servern von Hetzner einen stabilen und skalierbaren Kubernetes-Cluster einrichten können. Dabei gehen wir Schritt für Schritt durch alle notwendigen Konfigurationen und Best Practices – vom Server-Setup bis hin zur Bereitstellung von Anwendungen. Wir werden folgende Themen abdecken:</p>

<ol>
  <li><strong>Vorbereitungen und Anforderungen</strong> – Welche Voraussetzungen sollten erfüllt sein, und wie wählen wir die richtige Serverkonfiguration?</li>
  <li><strong>Installation von Proxmox</strong> – Eine detaillierte Anleitung zur Installation und Konfiguration von Proxmox auf den Hetzner-Servern.</li>
  <li><strong>Konfiguration von Proxmox</strong> – Wie wir den Proxmox Cluster für unseren Kubernetes Cluster perfekt vorbereiten.</li>
  <li><strong>Installation von Kubernetes</strong> – Eine detaillierte Anleitung zur Installation und Konfiguration von Kubernetes auf den Hetzner-Servern.</li>
  <li><strong>Konfiguration von Kubernetes</strong> – Die Konfiguration unseres Kubernetes Clusters.</li>
</ol>

<p>Lasst uns gemeinsam den gesamten Prozess durchlaufen, damit euer Kubernetes-Cluster auf Hetzner bereit für den produktiven Einsatz ist!</p>

<h1 id="vorbereitungen-und-anforderungen">Vorbereitungen und Anforderungen</h1>

<p>Um einen produktiven Kubernetes-Cluster auf Hetzner aufzubauen, benötigen wir mindestens drei Dedicated Server, die idealerweise über die folgende Ausstattung verfügen.</p>

<p>Für die gesamte Infrastruktur:</p>
<ul>
  <li><strong>(Optional) 12-Port-10G-Switch:</strong> Ein solcher Switch wird benötigt, wenn wir ein dediziertes 10G-Netzwerk für unseren Cluster planen. Somit können wir 10G Bandbreite für Ceph und 10G Bandbreite für unser SDN bereitstellen. Der Netzwerk-Uplink sollte auch über den Switch erfolgen.</li>
  <li><strong>(Optional) 8-Port-1G-Switch:</strong> Über diesen Switch können wir die Cluster-Kommunikation für Proxmox vornehmen.</li>
</ul>

<p>Pro Node:</p>
<ul>
  <li><strong>Sollten im gleichen Rack bereitgestellt werden</strong></li>
  <li><strong>(Optional) Benötigen (vorerst) keine öffentliche IPv4 Adressen</strong></li>
  <li><strong>Mindestens 2x zusätzliche Festplatten:</strong> Diese sind optional, aber für die Nutzung eines verteilten Speichersystems wie Ceph erforderlich. Alternativ kann auch lokaler LVM-Storage verwendet werden.</li>
  <li><strong>(Optional) Intel X520-DA2 mit mindestens 2x 10G-Schnittstellen:</strong> Beide 10G Uplinks mit dem 10G Switch verbinden.</li>
  <li><strong>(Optional) OnBoard NIC 1G:</strong> Alle Server mit 1G Switch verbinden</li>
</ul>

<p>Die zusätzlichen Optionen sollten während der Bestellung im Bemerkungsfeld angegeben werden, da es sich hierbei nicht um eine Standardbestellung handelt. Ein Hetzner-Datacenter-Techniker wird diese speziellen Anforderungen individuell bearbeiten.</p>

<p>Eine kostengünstige Beispielkonfiguration könnte wie folgt aussehen. Beachte, dass du natürlich selbst entscheiden kannst, welches System am besten zu deinen Anforderungen passt.</p>

<p><img src="/assets/2024-11-13-hetzner-k8s-sdn/screen-1-example-config.png" alt="screen-1-example-config" style="display:block; margin-left:auto; margin-right:auto" height="500" /></p>

<p><strong>Hinweis:</strong> Nach der Installation der Hardware empfiehlt es sich, die kostenlosen Storage-Boxen BX10 für die Server zu aktivieren und darauf SAMBA/CIFS einzurichten. Diese zusätzlichen Speicherlösungen eignen sich hervorragend, um Images und Backups sicher extern zu speichern. Für den direkten Einsatz als VM- oder Container-Diskspeicher sind sie jedoch aufgrund von Leistungs- und Latenzbeschränkungen weniger empfehlenswert.</p>

<h1 id="installation-von-proxmox">Installation von Proxmox</h1>

<p>Die einzelnen Server sind nun bereitgestellt und entweder über eine öffentliche IPv4- oder IPv6-Adresse per SSH erreichbar. Aktuell befinden wir uns im Hetzner Rescue-System, einem Linux-basierten System, das über PXE gebootet wird. Dieses Rescue-System ermöglicht es uns, grundlegende Wartungsarbeiten durchzuführen, wie beispielsweise das Mounten der Server-Disks. Zudem können wir über den Befehl <code class="language-plaintext highlighter-rouge">chroot</code> in das Dateisystem des Servers wechseln, um Konfigurationen oder Reparaturen direkt am installierten Betriebssystem vorzunehmen. <a href="https://docs.hetzner.com/de/robot/dedicated-server/troubleshooting/hetzner-rescue-system/">Mehr zu dem Hetzner Rescue-System hier</a>.</p>

<p>Wir werden das Proxmox-on-Debian-Bookworm-Image mithilfe des Tools <code class="language-plaintext highlighter-rouge">installimage</code> installieren und dabei das Betriebssystem nach unseren Anforderungen konfigurieren. Achte darauf, die Konfiguration entsprechend deiner Infrastruktur anzupassen – dies betrifft insbesondere die Festplatteneinstellungen, das Software-RAID und das LVM-Setup. Passe die Partitionierung und Speicherstruktur so an, dass sie optimal zu deinem Setup und den spezifischen Anforderungen deiner Serverumgebung passen.</p>

<p>Achte darauf, dass der Hostname ein eindeutiger FQDN (Fully Qualified Domain Name) ist, also eine vollständige Domain, die auf die öffentliche IPv4- oder IPv6-Adresse verweist und von außen erreichbar ist. Andernfalls kann es zu Problemen im Proxmox-Cluster kommen, da dieser auf die Erreichbarkeit der Hostnamen angewiesen ist. Es empfiehlt sich außerdem, alle Systeme im Cluster in der Datei <code class="language-plaintext highlighter-rouge">/etc/hosts</code> zu hinterlegen, um eine stabile Namensauflösung innerhalb des Netzwerks zu gewährleisten.</p>

<p>Du kannst einzelne Festplatten mithilfe des Symbols <code class="language-plaintext highlighter-rouge">#</code> von der Konfiguration ausschließen, was besonders für den Einsatz mit Ceph empfohlen wird. Wenn du hingegen nur ein Local-LVM verwenden möchtest, kannst du diese Festplatten in die Konfiguration aufnehmen. Du hast außerdem die Flexibilität, selbst zu entscheiden, ob du die NVMe-SSDs für Ceph oder für den lokalen Speicher nutzen möchtest. Diese Einstellungen sind anpassbar und sollten entsprechend deinen Anforderungen und der gewünschten Speicherarchitektur vorgenommen werden.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DRIVE 1 /dev/sda
DRIVE 2 /dev/sda
#DRIVE 3 /dev/sdb
#DRIVE 3 /dev/sdc

SWRAID 1
SWRAIDLEVEL 1

BOOTLOADER grub

HOSTNAME proxmox0[1-3].[deinedomain]

PART   /boot  ext3        2G
PART   lvm    proxmox    all

LV  proxmox  root  /     ext4  50G
LV  proxmox  swap  swap  swap  16G
LV  proxmox  home  /home xfs   15G
LV  proxmox  opt   /opt  xfs   15G
LV  proxmox  var   /var  xfs   50G

IMAGE /root/images/Debian-bookworm-latest-amd64-base.tar.gz
</code></pre></div></div>

<p>Nachdem der Server erfolgreich installiert wurde und Proxmox gebootet ist, können wir mit der Konfiguration der Proxmox-Infrastruktur fortfahren. In den nächsten Schritten richten wir die Netzwerkparameter, die Cluster-Einstellungen sowie die Speicher- und Backup-Optionen ein, um eine stabile und skalierbare Umgebung für unsere Virtualisierungs- und Container-Workloads zu schaffen.</p>

<h1 id="konfiguration-von-proxmox">Konfiguration von Proxmox</h1>

<h2 id="hetzner-vswitch">Hetzner vSwitch</h2>
<p>Für die weitere Konfiguration wechseln wir erneut in den Hetzner Robot und richten dort einen vSwitch ein. Dieser befindet sich auf der Übersichtsseite der Dedicated Server. Wir wählen einen Namen sowie eine VLAN-ID aus; in diesem Beispiel verwenden wir die VLAN-ID <code class="language-plaintext highlighter-rouge">4000</code> und fügen alle unsere Server in den vSwitch ein.</p>

<p>Im nächsten Schritt konfigurieren wir den vSwitch in der Datei <code class="language-plaintext highlighter-rouge">/etc/network/interfaces</code>. Achte darauf, dass sich die Namen deiner Netzwerkinterfaces sowie die IP-Adressen je nach Serverkonfiguration unterscheiden können. Passe die Konfiguration daher an deine spezifischen Netzwerkdetails an.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>auto lo
iface lo inet loopback

iface lo inet6 loopback

auto enp35s0
iface enp35s0 inet static
        address [SERVER IPv4 ADDRESS CIDR]
        gateway [SERVER IPv4 GATEWAY]
# Public Network v4

iface enp35s0 inet6 static
        address [SERVER IPv6 ADDRESS CIDR]
        gateway fe80::1
# Public Network v6

auto enp35s0.4000
iface enp35s0.4000 inet manual
# vSwitch VLAN

auto vmbr4000
iface vmbr4000 inet static
        address 10.0.1.2/24 # 10.0.1.2 für proxmox01, 10.0.1.3 für proxmox02 und 10.0.1.4 für proxmox03
        bridge-ports enp35s0.4000
        bridge-stp off
        bridge-fd 0
        mtu 1400
        up route add -net 10.0.0.0 netmask 255.255.0.0 gw 10.0.1.1 dev vmbr4000
        post-up iptables -t nat -A POSTROUTING -s '10.0.1.0/24' -o enp35s0 -j MASQUERADE
        post-down iptables -t nat -D POSTROUTING -s '10.0.1.0/24' -o enp35s0 -j MASQUERADE
# vSwitch vmbr4000

source /etc/network/interfaces.d/*
</code></pre></div></div>

<p>In dieser Netzwerk-Konfiguration richten wir zudem NAT ein, sodass VMs im <code class="language-plaintext highlighter-rouge">vmbr4000</code>-Netzwerk über das Internet kommunizieren können. Da unsere Kubernetes-Nodes keine öffentlichen IPv4-Adressen benötigen, reicht es aus, ihnen den Zugriff auf das Internet über NAT bereitzustellen.</p>

<p>Die Server sollten nun über die IPv4 Adressen erreichbar sein:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ping 10.0.1.3
ping 10.0.1.4
</code></pre></div></div>

<p>Zusätzlich besteht die Möglichkeit, den vSwitch bei Bedarf mit unseren Hetzner Cloud VMs zu verknüpfen. Dadurch können wir eine nahtlose Verbindung zwischen den Dedicated Servern und den Cloud-Instanzen herstellen, was besonders nützlich ist, wenn Kubernetes-Cluster über mehrere Umgebungen hinweg skaliert werden sollen oder zusätzliche Ressourcen aus der Cloud eingebunden werden. Diese Verknüpfung ermöglicht eine flexible, hybride Infrastruktur, in der sowohl Dedicated Server als auch Cloud-Ressourcen effizient in einem gemeinsamen Netzwerk arbeiten.</p>

<h2 id="proxmox-cluster">Proxmox Cluster</h2>

<p>Da die Systeme nun miteinander kommunizieren können, stelle sicher, dass alle Server in der Datei <code class="language-plaintext highlighter-rouge">/etc/hosts</code> eingetragen sind. Verwende dafür die IP-Adressen aus dem <code class="language-plaintext highlighter-rouge">10.0.1.0/24</code>-Bereich, da wir über dieses Subnetz die Cluster-Kommunikation aufbauen möchten.</p>

<p>Auf dem Server <code class="language-plaintext highlighter-rouge">proxmox01</code> gehen wir zu <strong>Datacenter -&gt; Cluster -&gt; Create Cluster</strong> und wählen als Cluster-Netzwerk unser 1G-Netzwerk aus.</p>

<p>Auf den Servern <code class="language-plaintext highlighter-rouge">proxmox02</code> und <code class="language-plaintext highlighter-rouge">proxmox03</code> verwenden wir die <code class="language-plaintext highlighter-rouge">Join Information</code>, um dem Cluster beizutreten. Achte dabei darauf, immer das richtige Cluster-Netzwerk auszuwählen, um eine stabile und einheitliche Verbindung im gesamten Cluster sicherzustellen.</p>

<h2 id="proxmox-sdn">Proxmox SDN</h2>

<p>Um das Proxmox Software Defined Networking (SDN) zu aktivieren, müssen wir zunächst einige zusätzliche Pakete installieren, die in der Standard-Proxmox-Installation nicht enthalten sind. Diese Pakete erweitern die Netzwerkkapazitäten und erlauben die Konfiguration von SDN-Features</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt <span class="nt">-y</span> update
apt <span class="nt">-y</span> <span class="nb">install </span>libpve-network-perl frr-pythontools
</code></pre></div></div>

<p>Damit haben wir nun die Grundlage geschaffen, das Software-Defined Network (SDN) direkt über die Proxmox-Oberfläche weiter zu konfigurieren. So können wir Netzwerkressourcen zentral verwalten, virtuelle Netzwerke einrichten und nahtlos auf die erweiterten SDN-Funktionen zugreifen, um den Netzwerkverkehr flexibel und effizient zu steuern.</p>

<p>Als nächsten Schritt wechseln wir in die Proxmox-Oberfläche und navigieren zu <strong>Datacenter -&gt; SDN -&gt; Options</strong>, um dort einen <code class="language-plaintext highlighter-rouge">evpn</code>-Controller zu erstellen. Dieser <code class="language-plaintext highlighter-rouge">evpn</code>-Controller ermöglicht die Nutzung eines Ethernet VPNs innerhalb der SDN-Umgebung, wodurch virtuelle Netzwerke effizient über mehrere Hosts hinweg verwaltet und verteilt werden können.</p>

<ul>
  <li><strong>ASN:</strong> Wert <code class="language-plaintext highlighter-rouge">65000</code> belassen.</li>
  <li><strong>Peers:</strong> Dort werden alle Proxmox Nodes eingetragen und alle Kubernetes Master Nodes (ich nutze hier den Bereich <code class="language-plaintext highlighter-rouge">10.0.2.21 - 10.0.2.23</code> für meine Kubernetes Master). Beispiel-Wert: <code class="language-plaintext highlighter-rouge">10.0.1.2,10.0.1.3,10.0.1.4,10.0.2.21,10.0.2.22,10.0.2.23</code>.</li>
</ul>

<p>Als nächsten Schritt navigieren wir zu <strong>Datacenter -&gt; SDN -&gt; Zones</strong> und erstellen eine EVPN-Zone. Hier sind einige Beispielwerte für die Konfiguration:</p>
<ul>
  <li><strong>ID:</strong> <code class="language-plaintext highlighter-rouge">vnet</code></li>
  <li><strong>Controller:</strong> den vorher erstellten <code class="language-plaintext highlighter-rouge">bgp</code></li>
  <li><strong>VRF-VXLAN Tag:</strong> <code class="language-plaintext highlighter-rouge">4005</code></li>
  <li><strong>Exit Nodes:</strong> <code class="language-plaintext highlighter-rouge">proxmox01, proxmox02, proxmox03</code></li>
  <li><strong>Primary Exit Node:</strong> <code class="language-plaintext highlighter-rouge">proxmox01</code></li>
  <li><strong>Exit Nodes Local Routing:</strong> <code class="language-plaintext highlighter-rouge">Nein</code></li>
  <li><strong>Advertise Subnets:</strong> <code class="language-plaintext highlighter-rouge">Ja</code></li>
  <li><strong>Disable ARP-nd Suppression:</strong> <code class="language-plaintext highlighter-rouge">Ja</code></li>
  <li><strong>MTU</strong> <code class="language-plaintext highlighter-rouge">1400</code></li>
</ul>

<p>Alle weiteren Einstellungen können unverändert bleiben oder bei Bedarf an deine spezifischen Anforderungen angepasst werden. So hast du die Flexibilität, die <code class="language-plaintext highlighter-rouge">EVPN</code>-Zone optimal auf die Bedürfnisse deines Netzwerks und Workloads abzustimmen.</p>

<p>Diese Konfiguration ermöglicht eine effiziente Verteilung des Netzwerks über die definierten Knoten und stellt sicher, dass die <code class="language-plaintext highlighter-rouge">EVPN</code>-Zone als logisches Netzwerk im gesamten Proxmox-Cluster verfügbar ist. Dadurch können die Kubernetes-Nodes und Proxmox die verwendeten IP-Adressen nahtlos miteinander austauschen, was eine reibungslose Kommunikation und Verwaltung der Netzwerkressourcen innerhalb des Clusters gewährleistet.</p>

<h2 id="proxmox-storage">Proxmox Storage</h2>

<p>Falls ihr Ceph verwenden möchtet, könnt ihr die Installation einfach über die Proxmox-Oberfläche unter <strong>Datacenter -&gt; Ceph</strong> durchführen. Nach der Installation lassen sich die OSDs und weitere Ceph-Komponenten flexibel konfigurieren und einrichten. Eine detaillierte Anleitung zur kompletten Ceph-Konfiguration würde jedoch den Rahmen dieses Artikels sprengen.</p>

<p>Ich nutze zusätzlich die kostenlosen Storage-Boxen als externen Speicher. Diese bieten eine kosteneffiziente Möglichkeit, Backups, Images und andere wichtige Daten sicher und außerhalb des Hauptservers zu speichern.</p>

<p>Darüber hinaus sollte auch der <code class="language-plaintext highlighter-rouge">local-lvm</code>-Speicher angelegt werden. Dazu wählt ihr die Volume Group <code class="language-plaintext highlighter-rouge">proxmox</code> aus. Dieser Speicherbereich kann nun für VMs genutzt werden und dient als zusätzlicher Speicherplatz, der bei der Installation noch nicht definiert wurde.</p>

<h2 id="debian-cloud-image">Debian Cloud Image</h2>

<p>Um ein Debian Cloud Image für unsere Kubernetes-Nodes bereitzustellen, könnt ihr die folgenden Befehle verwenden:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /var/lib/vz/images
wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
qm create 9000 <span class="nt">--memory</span> 4096 <span class="nt">--net0</span> virtio,bridge<span class="o">=</span>vmbr4000 <span class="nt">--scsihw</span> virtio-scsi-pci
qm <span class="nb">set </span>9000 <span class="nt">--scsi0</span> local-lvm:0,import-from<span class="o">=</span>/var/lib/vz/images/debian-12-generic-amd64.qcow2
qm <span class="nb">set </span>9000 <span class="nt">--ide2</span> local-lvm:cloudinit
qm <span class="nb">set </span>9000 <span class="nt">--boot</span> <span class="nv">order</span><span class="o">=</span>scsi0
qm <span class="nb">set </span>9000 <span class="nt">--serial0</span> socket <span class="nt">--vga</span> serial0
qm template 9000
</code></pre></div></div>

<p>Dieser Prozess lädt das aktuelle Debian-Cloud-Image für “Bookworm” herunter und erstellt eine Vorlage (<code class="language-plaintext highlighter-rouge">template</code>) für die Kubernetes-Nodes mit 4 GB RAM und einer Netzwerkschnittstelle, die über <code class="language-plaintext highlighter-rouge">vmbr4000</code> kommuniziert. Anschließend wird das Image als <code class="language-plaintext highlighter-rouge">scsi0</code> eingebunden und das <code class="language-plaintext highlighter-rouge">cloudinit</code>-Laufwerk eingerichtet, um eine Cloud-Init-Konfiguration zu ermöglichen. Mit diesen Einstellungen steht das Image als VM-Vorlage bereit und kann für den schnellen Start weiterer Kubernetes-Nodes genutzt werden.</p>

<h1 id="installation-von-kubernetes">Installation von Kubernetes</h1>

<p>Nun erstellen wir auf jedem unserer Server basierend auf dem Debian Cloud Image jeweils einen Master-Node und einen Worker-Node für den Kubernetes-Cluster. Für die Master-Nodes nutzen wir die IP-Range <code class="language-plaintext highlighter-rouge">10.0.2.21 - 10.0.2.23</code>. Die IP-Adresse <code class="language-plaintext highlighter-rouge">10.0.2.20</code> wird als Cluster VIP (Virtual IP) reserviert und bleibt frei, um eine hochverfügbare Cluster-Verwaltung zu ermöglichen. Die Worker-Nodes verwenden die IP-Range <code class="language-plaintext highlighter-rouge">10.0.2.31 - 10.0.2.33</code>.</p>

<p>Diese Konfiguration stellt sicher, dass die Master- und Worker-Nodes über dedizierte IP-Adressen kommunizieren können und die Cluster VIP zur Verfügung steht, um den Cluster stabil zu halten und automatisch auf einen anderen Master-Node umzuschalten, falls dies erforderlich ist.</p>

<p>Mit dem in Proxmox integrierten Cloud-Init-Tool können wir grundlegende Einstellungen wie Hostname, Benutzername, SSH-Keys, IP-Konfiguration und mehr direkt an die VM übergeben. Dadurch wird die Ersteinrichtung der VM automatisiert und es sind keine zusätzlichen manuellen Konfigurationen erforderlich. Cloud-Init erleichtert die Verwaltung und Anpassung der Kubernetes-Nodes, sodass sie sofort einsatzbereit sind.</p>

<p>Ich verwende das Tool <code class="language-plaintext highlighter-rouge">k3sup</code>, das es mir ermöglicht, auf einfache Weise und direkt von meinem lokalen PC aus einen Kubernetes-Cluster zu erstellen. Mit <code class="language-plaintext highlighter-rouge">k3sup</code> lassen sich Kubernetes-Installationen automatisieren und die Knoten schnell verbinden, was den Aufbau eines Clusters erheblich vereinfacht.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k3sup <span class="nb">install</span> <span class="nt">--ip</span> 10.0.2.21 <span class="nt">--tls-san</span> 10.0.2.20 <span class="nt">--cluster</span> <span class="nt">--k3s-channel</span> latest <span class="nt">--k3s-extra-args</span> <span class="s2">"--disable servicelb"</span> <span class="nt">--local-path</span> <span class="nv">$HOME</span>/.kube/config <span class="nt">--user</span> kube
</code></pre></div></div>

<p>Sobald der erste Master-Node bereit ist, installieren wir <code class="language-plaintext highlighter-rouge">kube-vip</code>, um die Hochverfügbarkeit des Clusters zu gewährleisten. Zunächst benötigen wir die RBAC-Datei (Role-Based Access Control), um die erforderlichen Berechtigungen für <code class="language-plaintext highlighter-rouge">kube-vip</code> festzulegen. Anschließend erstellen wir die Konfigurationsdatei, die die Einstellungen für den VIP (Virtual IP) und die Kommunikation zwischen den Master-Nodes enthält.</p>

<p>Alle folgenden Befehle werden auf unserem Kubernetes-Master-Node 01 (10.0.2.21) ausgeführt:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> https://kube-vip.io/manifests/rbac.yaml

<span class="nb">export </span><span class="nv">VIP</span><span class="o">=</span>10.0.2.20
<span class="nb">export </span><span class="nv">INTERFACE</span><span class="o">=</span>lo
<span class="nv">KVVERSION</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-sL</span> https://api.github.com/repos/kube-vip/kube-vip/releases | jq <span class="nt">-r</span> <span class="s2">".[0].name"</span><span class="si">)</span>
<span class="nb">alias </span>kube-vip<span class="o">=</span><span class="s2">"ctr image pull ghcr.io/kube-vip/kube-vip:</span><span class="nv">$KVVERSION</span><span class="s2">; ctr run --rm --net-host ghcr.io/kube-vip/kube-vip:</span><span class="nv">$KVVERSION</span><span class="s2"> vip /kube-vip"</span>

kube-vip manifest daemonset <span class="se">\</span>
    <span class="nt">--interface</span> <span class="nv">$INTERFACE</span> <span class="se">\</span>
    <span class="nt">--address</span> <span class="nv">$VIP</span> <span class="se">\</span>
    <span class="nt">--inCluster</span> <span class="se">\</span>
    <span class="nt">--taint</span> <span class="se">\</span>
    <span class="nt">--controlplane</span> <span class="se">\</span>
    <span class="nt">--services</span> <span class="se">\</span>
    <span class="nt">--bgp</span> <span class="se">\</span>
    <span class="nt">--localAS</span> 65000 <span class="se">\</span>
    <span class="nt">--bgpRouterID</span> 10.0.2.20 <span class="se">\</span>
    <span class="nt">--bgppeers</span> 10.0.1.2:65000::false,10.0.1.3:65000::false,10.0.1.4:65000::false | <span class="nb">tee</span> /var/lib/rancher/k3s/server/manifests/kube-vip.yaml
</code></pre></div></div>

<p>In der Option <code class="language-plaintext highlighter-rouge">bgppeers</code> haben wir unsere Proxmox-Nodes hinterlegt. Dadurch kann der Kubernetes-Cluster direkt mitteilen, welche VIPs (Virtual IPs) über BGP (Border Gateway Protocol) angekündigt werden sollen. Diese Konfiguration sorgt dafür, dass die VIPs automatisch über das Netzwerk verteilt werden und die Proxmox-Nodes stets wissen, welche IPs aktiv im Cluster verwendet werden. Dies erhöht die Ausfallsicherheit und verbessert die Netzwerkkommunikation im Cluster erheblich.</p>

<p>Nun fügen wir die restlichen Master-Nodes mit <code class="language-plaintext highlighter-rouge">k3sup</code> hinzu:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k3sup <span class="nb">join</span> <span class="nt">--ip</span> 10.0.2.22 <span class="nt">--server-ip</span> 10.0.2.20 <span class="nt">--server</span> <span class="nt">--k3s-channel</span> latest <span class="nt">--user</span> kube
k3sup <span class="nb">join</span> <span class="nt">--ip</span> 10.0.2.23 <span class="nt">--server-ip</span> 10.0.2.20 <span class="nt">--server</span> <span class="nt">--k3s-channel</span> latest <span class="nt">--user</span> kube
</code></pre></div></div>

<p>Anschließend fügen wir die Worker-Nodes hinzu:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k3sup <span class="nb">join</span> <span class="nt">--ip</span> 10.0.2.31 <span class="nt">--server-ip</span> 10.0.2.20 <span class="nt">--k3s-channel</span> latest <span class="nt">--user</span> kube
k3sup <span class="nb">join</span> <span class="nt">--ip</span> 10.0.2.32 <span class="nt">--server-ip</span> 10.0.2.20 <span class="nt">--k3s-channel</span> latest <span class="nt">--user</span> kube
k3sup <span class="nb">join</span> <span class="nt">--ip</span> 10.0.2.33 <span class="nt">--server-ip</span> 10.0.2.20 <span class="nt">--k3s-channel</span> latest <span class="nt">--user</span> kube
</code></pre></div></div>

<p>Diese Befehle verbinden die zusätzlichen Master- und Worker-Nodes nahtlos mit dem Cluster, wobei die Cluster-Kommunikation über die VIP <code class="language-plaintext highlighter-rouge">10.0.2.20</code> läuft. So wird die Hochverfügbarkeit gewährleistet, da die Master-Nodes miteinander synchronisiert werden und die Worker-Nodes ihre Anweisungen zentral über den Cluster erhalten.</p>

<p>Mit dem Befehl <code class="language-plaintext highlighter-rouge">kubectl get nodes</code> lässt sich überprüfen, ob alle Knoten erfolgreich dem Cluster beigetreten sind und korrekt arbeiten. Dieser Befehl zeigt eine Liste aller Master- und Worker-Nodes an und gibt ihren aktuellen Status sowie ihre Rollen im Cluster an. So können wir sicherstellen, dass die Clusterstruktur vollständig ist und alle Verbindungen korrekt hergestellt wurden.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get nodes
NAME   STATUS   ROLES                       AGE     VERSION
km1    Ready    control-plane,etcd,master   2d19h   v1.31.2+k3s1
km2    Ready    control-plane,etcd,master   2d19h   v1.31.2+k3s1
km3    Ready    control-plane,etcd,master   2d19h   v1.31.2+k3s1
kw1    Ready    &lt;none&gt;                      4h45m   v1.30.6+k3s1
kw2    Ready    &lt;none&gt;                      2d18h   v1.30.6+k3s1
kw3    Ready    &lt;none&gt;                      4h45m   v1.30.6+k3s1
</code></pre></div></div>

<p>Die Kubernetes-Installation ist damit abgeschlossen. Alle Knoten sind erfolgreich dem Cluster beigetreten und die grundlegende Konfiguration ist eingerichtet. Der Cluster ist nun einsatzbereit für das Deployment von Anwendungen und die Verwaltung über <code class="language-plaintext highlighter-rouge">kubectl</code>.</p>

<h1 id="konfiguration-von-kubernetes">Konfiguration von Kubernetes</h1>

<p>Um LoadBalancer in Kubernetes nutzen zu können, installieren wir zunächst den <code class="language-plaintext highlighter-rouge">kube-vip</code> Cloud Controller mit folgendem Befehl:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> https://raw.githubusercontent.com/kube-vip/kube-vip-cloud-provider/main/manifest/kube-vip-cloud-controller.yaml
</code></pre></div></div>

<p>Zusätzlich erstellen oder bearbeiten wir eine ConfigMap, um den IP-Bereich für die VIPs (Virtual IPs) festzulegen. In diesem Beispiel verwenden wir den Bereich <code class="language-plaintext highlighter-rouge">10.0.2.200-10.0.2.240</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create configmap <span class="nt">-n</span> kube-system kubevip <span class="nt">--from-literal</span> range-global<span class="o">=</span>10.0.2.200-10.0.2.240
</code></pre></div></div>

<p>Mit dieser Konfiguration können LoadBalancer-Dienste im Kubernetes-Cluster automatisch VIPs aus dem definierten IP-Bereich beziehen und so den externen Zugriff auf Anwendungen ermöglichen. Wir können in der ConfigMap auch öffentliche IP-Adressen aus Failover-Subnetzen eintragen, die wir über den vSwitch gebucht haben. Durch die Integration mit dem SDN und die Nutzung von BGP werden diese IP-Adressen korrekt im Netzwerk geroutet, sodass externe Zugriffe über LoadBalancer-Dienste im Kubernetes-Cluster möglich sind. Diese Flexibilität ermöglicht es uns, VIPs sowohl für interne als auch für öffentliche Dienste zu nutzen und dabei eine zuverlässige Netzwerkstruktur zu gewährleisten.</p>

<p>Wenn wir den mit k3s mitgelieferten Traefik als Ingress verwenden möchten, müssen wir diesen anpassen:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl edit service traefik <span class="nt">-n</span> kube-system
</code></pre></div></div>

<p>Anschließend ändern wir in der Konfiguration des Traefik-Ingress den Wert <code class="language-plaintext highlighter-rouge">spec.loadBalancerIP</code> und tragen die öffentliche IP-Adresse ein, über die der Ingress erreichbar sein soll. Damit wird der Traefik-Ingress öffentlich verfügbar und kann extern angesprochen werden.</p>

<p>Dank dieser Einstellung können wir in Kombination mit dem <code class="language-plaintext highlighter-rouge">cert-manager</code> automatisch SSL-Zertifikate erstellen und verwalten. Dadurch wird das Deployment von Anwendungen deutlich erleichtert, da HTTPS-Verbindungen zentral und automatisiert abgesichert sind, und die Verwaltung von Zertifikaten erfolgt gebündelt im Cluster.</p>

<h1 id="tests">Tests</h1>

<p>Mit diesem Beispiel-Deployment können wir testen, ob die Konfiguration wie gewünscht funktioniert.</p>

<p>Die Datei <code class="language-plaintext highlighter-rouge">whoami.yaml</code> definiert ein ReplicaSet mit drei Replikaten der <code class="language-plaintext highlighter-rouge">whoami</code>-Anwendung, einen Service zur Bereitstellung der Anwendung und einen Ingress, um die Anwendung extern verfügbar zu machen:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ReplicaSet</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">whoami</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">whoami</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">whoami</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">whoami</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">traefik/whoami</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">whoami</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">whoami</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">whoami</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">cert-manager.io/cluster-issuer</span><span class="pi">:</span> <span class="s">letsencrypt-cloudflare</span>
    <span class="na">nginx.ingress.kubernetes.io/rewrite-target</span><span class="pi">:</span> <span class="s">/</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">tls</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">whoami.peppermint.cloud</span>
      <span class="na">secretName</span><span class="pi">:</span> <span class="s">whoami-peppermint-cloud</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">http</span><span class="pi">:</span>
        <span class="na">paths</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
            <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
            <span class="na">backend</span><span class="pi">:</span>
              <span class="na">service</span><span class="pi">:</span>
                <span class="na">name</span><span class="pi">:</span> <span class="s">whoami</span>
                <span class="na">port</span><span class="pi">:</span>
                  <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<h3 id="hinweise">Hinweise:</h3>
<ul>
  <li><strong>Domain</strong>: Passe die Domain in <code class="language-plaintext highlighter-rouge">tls.hosts</code> auf diejenige an, die öffentlich erreichbar ist.</li>
  <li><strong>ClusterIssuer</strong>: Falls der <code class="language-plaintext highlighter-rouge">cert-manager</code> nicht installiert ist oder ein anderer Issuer verwendet werden soll, entferne oder ändere die Annotation <code class="language-plaintext highlighter-rouge">cert-manager.io/cluster-issuer</code>.</li>
</ul>

<p>Durch dieses Deployment wird die <code class="language-plaintext highlighter-rouge">whoami</code>-Anwendung über den Traefik-Ingress öffentlich verfügbar und sollte HTTPS-Zertifikate über <code class="language-plaintext highlighter-rouge">letsencrypt-cloudflare</code> erhalten, falls alles korrekt eingerichtet ist.</p>

<p>Wir können die Funktionalität des Deployments mit einem einfachen <code class="language-plaintext highlighter-rouge">curl</code>-Befehl überprüfen:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="s2">"https://whoami.peppermint.cloud"</span>
</code></pre></div></div>

<p>Das Ergebnis sollte dann in etwa wie folgt aussehen:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl "https://whoami.peppermint.cloud"
Hostname: whoami-dxrhk
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.8
IP: fe80::ec63:60ff:fee3:42ad
RemoteAddr: 10.42.0.7:36488
GET / HTTP/1.1
Host: whoami.peppermint.cloud
User-Agent: curl/7.88.1
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1
X-Forwarded-Host: whoami.peppermint.cloud
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-57b79cf995-d6ph7
X-Real-Ip: 10.42.0.1
</code></pre></div></div>

<p>Bei jeder neuen Ausführung von <code class="language-plaintext highlighter-rouge">curl</code> ändert sich der Hostname des Containers, der die Anfrage bearbeitet. Das zeigt, dass die Anwendung über mehrere Container repliziert wurde und diese nun auf verschiedenen Hosts verteilt sind. Dadurch wird eine hohe Verfügbarkeit gewährleistet und die Leistung der Anwendung kann durch die Verteilung der Anfragen auf mehrere Instanzen skaliert werden.</p>

<h1 id="erfolg">Erfolg</h1>

<p>Wir haben erfolgreich einen produktiven Kubernetes-Cluster mit BGP-Integration in unserer Proxmox-Infrastruktur eingerichtet. Damit ist die Grundlage für eine hochverfügbare und skalierbare Umgebung geschaffen.</p>

<p>In weiteren Artikeln werden wir uns noch detailliert mit der Einrichtung des Speichers für <strong>PersistentVolumeClaims</strong> sowie dem <strong>HorizontalPodAutoscaling</strong> beschäftigen, um die volle Leistungsfähigkeit des Clusters auszuschöpfen.</p>

<p><img src="/assets/borat-success.jpg" alt="borat-success" style="display:block; margin-left:auto; margin-right:auto" height="250" /></p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="kubernetes" /><category term="hetzner" /><category term="bgp" /><category term="proxmox" /><summary type="html"><![CDATA[Einleitung]]></summary></entry><entry><title type="html">Kubernetes Preisvergleich</title><link href="https://docs.pazdzewicz.de/kubernetes/2023/12/10/kubernetes-pricing.html" rel="alternate" type="text/html" title="Kubernetes Preisvergleich" /><published>2023-12-10T20:15:00+00:00</published><updated>2023-12-10T20:15:00+00:00</updated><id>https://docs.pazdzewicz.de/kubernetes/2023/12/10/kubernetes-pricing</id><content type="html" xml:base="https://docs.pazdzewicz.de/kubernetes/2023/12/10/kubernetes-pricing.html"><![CDATA[<h1 id="einleitung">Einleitung</h1>

<p>Für meine Projekte benötige ich regelmäßig zuverlässige Kubernetes Cluster. In der Vergangenheit habe ich gerne managed Kubernetes von DigitalOcean verwendet, jedoch empfand ich dies gelegentlich als zu kostspielig. Selbst für kleinere Projekte musste ich manchmal mehr als 100 € pro Monat bezahlen. Daher habe ich beschlossen, eine Liste verschiedener Cloud-Provider zu erstellen, um die Preise besser vergleichen zu können.</p>

<p>Zusätzlich habe ich ein Bemerkungsfeld eingefügt, das unter anderem meine persönlichen Erfahrungen mit den jeweiligen Providern enthält. In die Liste habe ich nur Anbieter aufgenommen, die ich tatsächlich selbst ausprobiert habe und zu denen ich eine fundierte Meinung entwickeln konnte.</p>

<h2 id="einleitung---anwendungs-stack">Einleitung - Anwendungs-Stack</h2>

<p>Auch stütze ich meinen Preisvergleich auf die notwendigen Ressourcen eines selbst entwickelten Microservice-Anwendungs-Stacks aus folgenden Komponenten:</p>

<ul>
  <li>Frontend (Phalcon PHP Anwendung, Redis Cache, MySQL Datenbank)</li>
  <li>Backend (Phalcon PHP Anwendung, Redis Cache, MySQL Datenbank)</li>
  <li>CMS (Directus.io API, Redis Cache, PostgreSQL Datenbank)</li>
  <li>User Data (Directus.io API, Redis Cache, PostgreSQL Datenbank)</li>
  <li>Authentication (Keycloak, PostgreSQL Datenbank)</li>
  <li>Logging (Graylog, Elasticsearch, MongoDB Datenbank)</li>
</ul>

<p>Diverse Anwendungen in dem Stack liegen unter anderem als ReplicaSet vor, das heißt, es gibt mehrere Instanzen von z.B. der Frontend Phalcon PHP Anwendung. Einige, wie zum Beispiel Graylog, sind ausschließlich für administrative Zwecke bestimmt und haben keine Replikate.</p>

<p>Für ein Loader.io-Ergebnis mit 10.000 Benutzern benötigt dieser Anwendungs-Stack mindestens 3 Nodes, wobei jede Node über 4 CPU-Kerne und 16 GB RAM verfügt. Dies ist großzügig dimensioniert, um noch zusätzliche Ressourcen in Reserve zu haben.</p>

<p>Damit habe ich eine Real-Life-Workload abgedeckt, die für die meisten Firmen mit einem mittleren bis großen Online-Shop relevant ist.</p>

<h2 id="einleitung---unterteilung">Einleitung - Unterteilung</h2>

<p>Der Preisvergleich ist in zwei Kategorien unterteilt: Anbieter, die bereits vorkonfigurierte, also managed Kubernetes Cluster anbieten, und Anbieter, auf deren Plattform man Kubernetes Cluster selbst installieren kann.</p>

<h1 id="preisvergleich">Preisvergleich</h1>

<h2 id="preisvergleich---computing">Preisvergleich - Computing</h2>

<p>Ich betrachte derzeit nur die reinen Rechenkosten. Zusätzliche Features wie etwa S3-kompatibler Speicher, Blockspeicher, Loadbalancer und Backups kosten in der Regel einen Aufpreis. Möglicherweise aktualisiere ich dieses Dokument in der Zukunft und ziehe diese Kosten ebenfalls in Betracht.</p>

<h3 id="managed-kubernetes-cluster">Managed Kubernetes Cluster</h3>

<table>
  <thead>
    <tr>
      <th>Hosting Provider</th>
      <th>Standort</th>
      <th>Node Anzahl</th>
      <th>Node Flavor</th>
      <th>Preis pro Monat</th>
      <th>Bemerkung</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://cloud.google.com/">Google Cloud</a></td>
      <td>Frankfurt (europe-west3)</td>
      <td>3</td>
      <td>e2-standard-4 (4 Cores, 16 GB RAM, 20 GB)</td>
      <td>385,35 USD</td>
      <td> </td>
    </tr>
    <tr>
      <td><a href="https://aws.amazon.com/de/">Amazon Web Services</a></td>
      <td>Frankfurt (eu-central-1)</td>
      <td>3</td>
      <td>t4g.xlarge (4 Cores, 16 GB RAM, 20 GB)</td>
      <td>415,09 USD</td>
      <td>Support ist manchmal echt verdammt langsam, auch autoscaling kann ewig dauern.</td>
    </tr>
    <tr>
      <td><a href="https://www.digitalocean.com/">Digitalocean</a></td>
      <td>Frankfurt (FRA1)</td>
      <td>3</td>
      <td>s-4vcpu-16gb-amd (4 Cores, 16 GB RAM, 200 GB)</td>
      <td>252 USD</td>
      <td> </td>
    </tr>
    <tr>
      <td><a href="https://www.vultr.com/">Vultr</a></td>
      <td>Frankfurt</td>
      <td>3</td>
      <td>regular-6vcpu-16gb (6 Cores, 16 GB RAM, 320 GB)</td>
      <td>240 USD</td>
      <td> </td>
    </tr>
    <tr>
      <td><a href="https://www.ovhcloud.com/de/">OVH</a></td>
      <td>Frankfurt (DE1)</td>
      <td>3</td>
      <td>B2-15 (4 Cores, 15 GB RAM, 100 GB)</td>
      <td>138,60 € + MwSt = 164,95 €</td>
      <td> </td>
    </tr>
    <tr>
      <td><a href="https://www.ionos.de/">IONOS</a></td>
      <td>Frankfurt</td>
      <td>3</td>
      <td>amd-opteron (4 Cores, 16 GB RAM, 100 GB)</td>
      <td>363,32 €</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h3 id="unmanaged-kubernetes-cluster">Unmanaged Kubernetes Cluster</h3>

<table>
  <thead>
    <tr>
      <th>Hosting Provider</th>
      <th>Standort</th>
      <th>Node Anzahl</th>
      <th>Node Flavor</th>
      <th>Preis pro Monat</th>
      <th>Bemerkung</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://www.hetzner.com/de">Hetzner</a> - Cloud</td>
      <td>Nürnberg</td>
      <td>3</td>
      <td>CX41 (4 Cores, 16 GB RAM, 160 GB)</td>
      <td>62,12 €</td>
      <td><a href="https://community.hetzner.com/tutorials/install-kubernetes-cluster">Kubernetes muss selbst installiert werden</a></td>
    </tr>
    <tr>
      <td><a href="https://www.hetzner.com/de">Hetzner</a> - Dedicated</td>
      <td>Finnland</td>
      <td>3</td>
      <td>AX41-NVME (6 Cores, 64 GB RAM, 512 GB)</td>
      <td>133,17 €</td>
      <td><a href="https://kubesail.com/blog/dedicated-kubernetes-on-hetzner">Kubernetes muss selbst installiert werden</a></td>
    </tr>
  </tbody>
</table>

<h1 id="fazit">Fazit</h1>

<p>Kubernetes kann ziemlich kostspielig sein, aber es gibt deutliche Preisunterschiede zwischen den verschiedenen Providern. Und manchmal rechtfertigt sich der höhere Preis nicht unbedingt durch einen besseren Kundensupport oder mehr Stabilität. In diesem Zusammenhang bin ich äußerst positiv von Hetzner überrascht. Die Bereitstellung der Rechenleistung erfolgt nicht nur schnell, sondern auch der Support ist herausragend gut. Im Vergleich dazu musste ich bei Amazon Web Services teilweise wirklich lange auf die Bereitstellung warten (über 2 Stunden!) und auch auf eine Antwort des Supports (über 2 Tage). Das ist einfach nicht die hohen Kosten wert!</p>

<p>Natürlich muss jeder für sich entscheiden, welchen Provider er auswählt. Ich bleibe allerdings weiterhin Hetzner für größere Projekte treu!</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="kubernetes" /><summary type="html"><![CDATA[Einleitung]]></summary></entry><entry><title type="html">Drei externe Displays an einem Macbook Pro mit M1 Pro verwenden</title><link href="https://docs.pazdzewicz.de/apple/2023/11/12/macbook-tripple-screen.html" rel="alternate" type="text/html" title="Drei externe Displays an einem Macbook Pro mit M1 Pro verwenden" /><published>2023-11-12T19:00:00+00:00</published><updated>2023-11-12T19:00:00+00:00</updated><id>https://docs.pazdzewicz.de/apple/2023/11/12/macbook-tripple-screen</id><content type="html" xml:base="https://docs.pazdzewicz.de/apple/2023/11/12/macbook-tripple-screen.html"><![CDATA[<h1 id="einleitung">Einleitung</h1>

<p>Gemäß der <a href="https://support.apple.com/kb/SP858?viewlocale=de_DE&amp;locale=de_DE">technischen Dokumentation von Apple</a> ist es auf einem MacBook Pro aus dem Jahr 2021 mit M1 Pro Chip nicht möglich, mehr als zwei externe Displays zu verwenden. Diese technische Begrenzung kann frustrierend sein, insbesondere wenn man, wie ich, gerne drei externe Displays nutzen würde.</p>

<p>Ich benutze, von links nach rechts, folgende Displays: <a href="https://www.amazon.de/Dell-U2421HE-Zoll-1920x1080-entspiegelt/dp/B088P8DNK4">Dell U2421HE</a> (1920 x 1080 Pixel), <a href="https://www.lg.com/de/monitore/ultrawide/29um57-p/">LG Ultrawide 29UM57-P</a>, (2560 x 1080 Pixel) und erneut einen <a href="https://www.amazon.de/Dell-U2421HE-Zoll-1920x1080-entspiegelt/dp/B088P8DNK4">Dell U2421HE</a> (1920 x 1080 Pixel). Diese Bildschirme eignen sich perfekt für ein Triple-Monitor-Setup, da sie in der Höhe ideal aufeinander abgestimmt sind.</p>

<p>Die Dell-Monitore bieten eine beeindruckende Vielfalt an Anschlussmöglichkeiten, insbesondere der integrierte USB-C-Hub, der sich ideal eignet, um viele Geräte anzuschließen. Die unterschiedlichen Display-Anschlüsse, darunter DisplayPort über USB-C, DisplayPort und HDMI, ermöglichen es mir, mehrere PCs anzuschließen. Aktuell habe ich über DisplayPort einen Windows-Gaming-PC und über HDMI das MacBook angeschlossen. Dabei nutze ich den integrierten HDMI-Anschluss für das MacBook und einen weiteren HDMI-Anschluss, der im <a href="https://www.amazon.de/Satechi-Multi-Ports-Aluminium-Adapter-Kartenleser-grau-space-gray/dp/B075FW7H5J">Satechi ST-TCMA2M</a> Dock enthalten ist. Leider verfügt der LG-Monitor nur über zwei HDMI-Anschlüsse.</p>

<h1 id="ausprobieren">Ausprobieren</h1>

<p>Da ich von Natur aus ein Bastler bin und manchmal feststelle, dass technische Dokumentationen nicht immer die ganze Wahrheit abbilden, habe ich versucht, dies herauszufinden, indem ich ein zusätzliches <a href="https://www.delock.de/produkt/64074/merkmale.html">Delock 64074</a> Dock und einen <a href="https://www.anker.com/eu-de/products/a8312">Anker 310</a> USB-C zu HDMI Adapter verwendet habe. Die technischen Einschränkungen erwiesen sich mitunter als restriktiver als erhofft, und in diesem Fall hat sich gezeigt, dass die Angaben in der technischen Dokumentation von Apple korrekt sind.</p>

<h1 id="hilfeschrei">Hilfeschrei</h1>

<p>In meiner Verzweiflung habe ich mich auch an die in der Regel sehr hilfreiche Reddit-Community in <a href="https://www.reddit.com/r/de_EDV/">r/de_EDV</a> gewandt und einen <a href="https://www.reddit.com/r/de_EDV/comments/17oze2t/macbook_pro_2021_multi_displays">Beitrag</a> verfasst. Leider hat dies nicht zu dem gewünschten Ergebnis geführt.</p>

<h1 id="lösung">Lösung</h1>

<p>Zufällig bin ich auf einen <a href="https://www.startech.com/de-de/display-videoadapter/107b-usb-hdmi">StarTech.com 107B-USB-HDMI</a> Dual-Monitor-Adapter gestoßen, der es ermöglicht, über USB-C (und auch USB-A) zwei zusätzliche Displays anzuschließen – eines mit einer maximalen Auflösung von 1080p bei 60 Hz und eines mit 4k bei 30 Hz –, beide über HDMI verbunden. Das klang vielversprechend, und ich habe ihn sofort bei Amazon bestellt. Nach der Ankunft habe ich den Adapter sofort angeschlossen, die <a href="https://sgcdn.startech.com/005329/media/sets/USB32HDVGA/SMI-USB-Display-Software-for-macOS.zip">Treiber</a> von der Webseite für Mac OS heruntergeladen und installiert. Der LG Ultrawide Monitor lief problemlos im erweiterten Modus, und ich konnte endlich alle meine drei Displays nutzen.</p>

<p><img src="/assets/2023-11-12-macbook-tripple-screen/screen-tripple.PNG" alt="screen-tripple" style="display:block; margin-left:auto; margin-right:auto" /></p>

<h1 id="erleichterung">Erleichterung</h1>

<p>Ich freute mich außerordentlich und war erleichtert, dass ich nun endlich über die gesamte Bildschirmfläche verfügen konnte, die ich vor mir hatte. Mal wieder ein großartiger Erfolg!</p>

<p><img src="/assets/borat-success.jpg" alt="borat-success" style="display:block; margin-left:auto; margin-right:auto" height="250" /></p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="apple" /><summary type="html"><![CDATA[Einleitung Gemäß der technischen Dokumentation von Apple ist es auf einem MacBook Pro aus dem Jahr 2021 mit M1 Pro Chip nicht möglich, mehr als zwei externe Displays zu verwenden. Diese technische Begrenzung kann frustrierend sein, insbesondere wenn man, wie ich, gerne drei externe Displays nutzen würde. Ich benutze, von links nach rechts, folgende Displays: Dell U2421HE (1920 x 1080 Pixel), LG Ultrawide 29UM57-P, (2560 x 1080 Pixel) und erneut einen Dell U2421HE (1920 x 1080 Pixel). Diese Bildschirme eignen sich perfekt für ein Triple-Monitor-Setup, da sie in der Höhe ideal aufeinander abgestimmt sind. Die Dell-Monitore bieten eine beeindruckende Vielfalt an Anschlussmöglichkeiten, insbesondere der integrierte USB-C-Hub, der sich ideal eignet, um viele Geräte anzuschließen. Die unterschiedlichen Display-Anschlüsse, darunter DisplayPort über USB-C, DisplayPort und HDMI, ermöglichen es mir, mehrere PCs anzuschließen. Aktuell habe ich über DisplayPort einen Windows-Gaming-PC und über HDMI das MacBook angeschlossen. Dabei nutze ich den integrierten HDMI-Anschluss für das MacBook und einen weiteren HDMI-Anschluss, der im Satechi ST-TCMA2M Dock enthalten ist. Leider verfügt der LG-Monitor nur über zwei HDMI-Anschlüsse. Ausprobieren Da ich von Natur aus ein Bastler bin und manchmal feststelle, dass technische Dokumentationen nicht immer die ganze Wahrheit abbilden, habe ich versucht, dies herauszufinden, indem ich ein zusätzliches Delock 64074 Dock und einen Anker 310 USB-C zu HDMI Adapter verwendet habe. Die technischen Einschränkungen erwiesen sich mitunter als restriktiver als erhofft, und in diesem Fall hat sich gezeigt, dass die Angaben in der technischen Dokumentation von Apple korrekt sind. Hilfeschrei In meiner Verzweiflung habe ich mich auch an die in der Regel sehr hilfreiche Reddit-Community in r/de_EDV gewandt und einen Beitrag verfasst. Leider hat dies nicht zu dem gewünschten Ergebnis geführt. Lösung Zufällig bin ich auf einen StarTech.com 107B-USB-HDMI Dual-Monitor-Adapter gestoßen, der es ermöglicht, über USB-C (und auch USB-A) zwei zusätzliche Displays anzuschließen – eines mit einer maximalen Auflösung von 1080p bei 60 Hz und eines mit 4k bei 30 Hz –, beide über HDMI verbunden. Das klang vielversprechend, und ich habe ihn sofort bei Amazon bestellt. Nach der Ankunft habe ich den Adapter sofort angeschlossen, die Treiber von der Webseite für Mac OS heruntergeladen und installiert. Der LG Ultrawide Monitor lief problemlos im erweiterten Modus, und ich konnte endlich alle meine drei Displays nutzen. Erleichterung Ich freute mich außerordentlich und war erleichtert, dass ich nun endlich über die gesamte Bildschirmfläche verfügen konnte, die ich vor mir hatte. Mal wieder ein großartiger Erfolg!]]></summary></entry><entry><title type="html">Einen Cloudflare Tunnel erstellen</title><link href="https://docs.pazdzewicz.de/cloudflare/2023/09/24/cloudflare-tunnel.html" rel="alternate" type="text/html" title="Einen Cloudflare Tunnel erstellen" /><published>2023-09-24T20:00:00+00:00</published><updated>2023-09-24T20:00:00+00:00</updated><id>https://docs.pazdzewicz.de/cloudflare/2023/09/24/cloudflare-tunnel</id><content type="html" xml:base="https://docs.pazdzewicz.de/cloudflare/2023/09/24/cloudflare-tunnel.html"><![CDATA[<h1 id="einleitung">Einleitung</h1>

<p>Heute möchten wir einen einfachen Cloudflare Tunnel einrichten, um interne Dienste sicher hinter einer Firewall zu betreiben, ohne Ports im Internet freizugeben. Dafür benötigen wir selbstverständlich einen Cloudflare-Account und eine Domain, die bei Cloudflare konfiguriert ist.</p>

<p>Ein Cloudflare Tunnel ist eine Technologie, die es ermöglicht, private Netzwerke sicher mit dem Cloudflare-Netzwerk zu verbinden. Dies geschieht, indem der Datenverkehr von internen Diensten durch verschlüsselte Verbindungen zu Cloudflare-Servern geroutet wird. Dadurch können interne Dienste sicher und zuverlässig über das Internet erreichbar gemacht werden, ohne dass dafür Portweiterleitungen in der Firewall erforderlich sind. Dies erhöht die Sicherheit und schützt interne Ressourcen vor direkten externen Zugriffen.</p>

<h1 id="einen-tunnel-erstellen">Einen Tunnel erstellen</h1>

<p>Wir loggen uns in unser Cloudflare-Dashboard ein und erstellen unter <code class="language-plaintext highlighter-rouge">Zero Trust</code> -&gt; <code class="language-plaintext highlighter-rouge">Access</code> -&gt; <code class="language-plaintext highlighter-rouge">Tunnels</code> -&gt; <code class="language-plaintext highlighter-rouge">Create a tunnel</code> einen neuen Tunnel.</p>

<p>Jetzt vergeben wir einen Namen und klicken auf <code class="language-plaintext highlighter-rouge">Save tunnel</code>.</p>

<p>Nun kopieren wir uns den Tunnel Token:</p>

<p><img src="/assets/2023-09-24-cloudflare-tunnel/screen-1-token.PNG" alt="screen-1-token" /></p>

<p>Im nächsten Schritt installieren wir einen Connector. Die Pazdzewicz IT hat dafür einen eigenen optimierten Docker-Container entwickelt.</p>

<h1 id="connector-installieren">Connector Installieren</h1>

<p>Jetzt melden wir uns auf einem Linux-Server mit vorinstalliertem Docker an und klonen das Repository:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@gitlab.com:pazdzewicz_docker/cloudflared.git
cd cloudflared
</code></pre></div></div>

<p>Um den Connector verfügbar zu machen benötigen wir nun das Connector Token aus dem vorherigen Schritt und fügen diesen in unsere <code class="language-plaintext highlighter-rouge">.env</code> Datei ein:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cp .env.template .env
nano .env
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CLOUDFLARE_TOKEN="&lt;Tunnel Token&gt;"
</code></pre></div></div>

<p>Anschließend können wir den Tunnel mittels <code class="language-plaintext highlighter-rouge">docker-compose</code> starten:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose pull
docker-compose up -d
</code></pre></div></div>

<h1 id="verfügbarkeit-überprüfen">Verfügbarkeit überprüfen</h1>

<p>In der Übersicht der Tunnels überprüfen wir nun ob der Tunnel <code class="language-plaintext highlighter-rouge">HEALTHY</code> ist:</p>

<p><img src="/assets/2023-09-24-cloudflare-tunnel/screen-2-tunnels.PNG" alt="screen-2-tunnels" /></p>

<h1 id="routen-einrichten">Routen einrichten</h1>

<p>Nun klicken wir auf den Tunnel Namen und anschließend auf <code class="language-plaintext highlighter-rouge">Configure</code>. Unter <code class="language-plaintext highlighter-rouge">Public Hostname</code> -&gt; <code class="language-plaintext highlighter-rouge">Add a plublic hostname</code> können wir nun eine öffentliche Subdomain mit einem internen Service verbinden. In diesem Beispiel werden wir unseren internen Entwicklungsserver unter <code class="language-plaintext highlighter-rouge">dev.pazdzewicz.de</code> auf der IP <code class="language-plaintext highlighter-rouge">192.168.88.5</code> verfügbar machen.</p>

<p><img src="/assets/2023-09-24-cloudflare-tunnel/screen-3-hostname.PNG" alt="screen-3-hostname" /></p>

<p>Mit <code class="language-plaintext highlighter-rouge">Save hostname</code> können wir nun den Hostnamen abspeichern.</p>

<p>Nach dem aufrufen von <code class="language-plaintext highlighter-rouge">https://dev.pazdzewicz.de</code> im Web-Browser sollten wir unsere Seite sehen:</p>

<p><img src="/assets/2023-09-24-cloudflare-tunnel/screen-4-server.PNG" alt="screen-4-server" /></p>

<p>Somit ist unser Tunnel nun eingerichtet!</p>

<p><img src="/assets/borat-success.jpg" alt="borat-success" style="display:block; margin-left:auto; margin-right:auto" height="250" /></p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="cloudflare" /><summary type="html"><![CDATA[Einleitung]]></summary></entry><entry><title type="html">Minecraft Paper Server in Docker</title><link href="https://docs.pazdzewicz.de/docker/minecraft/2023/09/19/minecraft-paper-server.html" rel="alternate" type="text/html" title="Minecraft Paper Server in Docker" /><published>2023-09-19T18:00:00+00:00</published><updated>2023-09-19T18:00:00+00:00</updated><id>https://docs.pazdzewicz.de/docker/minecraft/2023/09/19/minecraft-paper-server</id><content type="html" xml:base="https://docs.pazdzewicz.de/docker/minecraft/2023/09/19/minecraft-paper-server.html"><![CDATA[<h1 id="einleitung">Einleitung</h1>

<p>Heute beschreiben wir, wie Sie ganz einfach und schnell einen Minecraft Paper Server in Docker erstellen können.</p>

<p>Minecraft Paper ist eine von der Community entwickelte Server-Software für das beliebte Sandbox-Spiel Minecraft. Es handelt sich um eine Modifikation (Mod) des offiziellen Minecraft-Servers, die auf eine verbesserte Leistung, Stabilität und erweiterte Funktionen abzielt. PaperMC baut auf dem sogenannten “Spigot”-Server auf, der bereits Optimierungen für Server-Performance bietet, und führt zusätzliche Anpassungen und Verbesserungen durch.</p>

<p>Die Hauptmerkmale von Minecraft Paper sind:</p>

<ul>
  <li>
    <p><strong>Verbesserte Leistung:</strong> PaperMC optimiert die Serverleistung erheblich, um eine flüssigere Spielerfahrung zu gewährleisten, insbesondere auf großen Servern mit vielen Spielern und Plugins.</p>
  </li>
  <li>
    <p><strong>Stabilität:</strong> Die Software behebt viele der bekannten Fehler und Absturzursachen des offiziellen Servers und bietet eine stabilere Umgebung für Spieler und Administratoren.</p>
  </li>
  <li>
    <p><strong>Plugin-Kompatibilität:</strong> PaperMC ist mit den meisten Minecraft-Plugins kompatibel, die für Spigot entwickelt wurden. Dies ermöglicht Serveradministratoren, eine breite Palette von Erweiterungen und Anpassungen hinzuzufügen, um ihre Spielerfahrung zu verbessern.</p>
  </li>
  <li>
    <p><strong>Aktive Entwicklung:</strong> Die PaperMC-Entwicklergemeinschaft arbeitet kontinuierlich an Updates und Verbesserungen, um sicherzustellen, dass der Server auf dem neuesten Stand bleibt und den Anforderungen der Minecraft-Community gerecht wird.</p>
  </li>
</ul>

<p>Insgesamt bietet Minecraft Paper eine solide Grundlage für die Erstellung und Verwaltung von Multiplayer-Minecraft-Servern mit verbesserter Leistung und erweiterten Funktionen.</p>

<h1 id="installation-des-docker-containers">Installation des Docker Containers</h1>

<p>Wir beginnen auf einem Linux-System, auf dem Docker und Docker-Compose bereits installiert sind, und klonen das GitLab-Repository für den Minecraft Paper Server.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@gitlab.com:pazdzewicz_docker/minecraft-paper.git
cd minecraft-paper/
</code></pre></div></div>

<p>Im Verzeichnis befindet sich bereits eine vorkonfigurierte <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> Datei, mit der der Server problemlos gestartet werden kann.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose pull
docker-compose up -d
</code></pre></div></div>

<h1 id="konfiguration-des-minecraft-servers">Konfiguration des Minecraft Servers</h1>

<p>Nun öffnen wir unseren Minecraft Client in der jeweiligen gewählten Version. Und Verbinden uns über <code class="language-plaintext highlighter-rouge">Multiplayer</code> -&gt; <code class="language-plaintext highlighter-rouge">Add Server</code>.</p>

<p><img src="/assets/2023-09-19-minecraft-paper-server/screen-1-add-server.PNG" alt="screen-1-add-server" /></p>

<p>Dies sollte aufgrund der aktivierten Whitelist nicht möglich sein, ist jedoch ein wichtiger Schritt für das weitere Vorgehen.</p>

<p>Jetzt rufen wir die Protokolle (Logs) des Containers auf.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose logs spigot
</code></pre></div></div>

<p>Wir benötigen den Namen und die UUID des Spielers und kopieren beide Informationen.</p>

<p><img src="/assets/2023-09-19-minecraft-paper-server/screen-2-logs.PNG" alt="screen-2-logs" /></p>

<p>Wir tragen diese Informationen nun in der Datei <code class="language-plaintext highlighter-rouge">settings/whitelist.json</code> ein:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
  {
    "uuid":"588e037c-17a6-41e6-ab0d-bbde572dde51",
    "name":"RageGuyKai"
  },
  {
    "uuid":"XXXXXX-XXXX-XXXXXX-XXX-XXXXXXXX",
    "name":"Nocheinspieler"
  }
]
</code></pre></div></div>

<p>Und wenn der Benutzer ein Server Operator (Administrator) sein soll, müssen wir die entsprechenden Informationen in der Datei <code class="language-plaintext highlighter-rouge">settings/ops.json</code> eintragen.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
  {
    "uuid": "588e037c-17a6-41e6-ab0d-bbde572dde51",
    "name": "RageGuyKai",
    "level": 4,
    "bypassesPlayerLimit": false
  },
  {
    "uuid":"XXXXXX-XXXX-XXXXXX-XXX-XXXXXXXX",
    "name":"Nocheinspieler",
    "level": 4,
    "bypassesPlayerLimit": false
  }
]
</code></pre></div></div>

<p>Lassen Sie uns jetzt den Server einmal neu starten, um die Einstellungen zu bestätigen:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose restart spigot
</code></pre></div></div>

<p>Jetzt können wir uns mit dem Minecraft-Server verbinden und unserer Kreativität gemeinsam mit Freunden freien Lauf lassen.</p>

<p><img src="/assets/borat-success.jpg" alt="borat-success" style="display:block; margin-left:auto; margin-right:auto" height="250" /></p>

<p>PS: Weitere Server Einstellungen können unter <code class="language-plaintext highlighter-rouge">config/</code> angepasst werden :smile:</p>]]></content><author><name>Kai Pazdzewicz</name><email>kai@pazdzewicz.de</email></author><category term="docker" /><category term="minecraft" /><summary type="html"><![CDATA[Einleitung]]></summary></entry></feed>