tva
← Insights

Reverse Proxies stapeln: Eine Produktionsarchitektur

Vier Reverse Proxies vor einer einzelnen Anwendung wirken auf den ersten Blick wie ein System, das vereinfacht werden müsste. Die meisten Tutorials zur Web-Produktionsinfrastruktur beschränken sich auf eine Schicht – Nginx, Caddy oder Traefik – und betrachten alles darüber als Over-Engineering. In der Realität löst jedoch jedes dieser Tools ein eigenständiges Problem, und wenn man die Arbeitsteilung versteht, löst sich die scheinbare Redundanz in etwas Kohärentes auf.

Dies ist die Architektur, die wir für mehrere Kunden-Deployments einsetzen: Cloudflare an der Edge, Traefik als container-bewusster Einstiegspunkt, Varnish für HTTP-Caching, Nginx als Anwendungs-Host und die Anwendung selbst am Ende der Kette. Der Request-Fluss ist:

Cloudflare → Traefik → Varnish → Nginx → Application

Im Folgenden wird erläutert, was jede Schicht beiträgt, wo die Grenzen liegen und welche Performance-Implikationen sich ergeben – einschließlich der Stellen, wo dieser Stack Overhead erzeugt, den ein einfacheres Setup nicht hätte.

Was jede Schicht tatsächlich tut

Cloudflare operiert an der Edge des öffentlichen Internets. Es absorbiert DDoS-Traffic, wendet WAF-Regeln an, terminiert TLS an global verteilten PoPs und cached statische Assets in Nutzernähe. Was es nicht gut kann, ist intelligente Routing-Entscheidungen über Ihre Container zu treffen oder Anwendungsantworten zu cachen, die eine Cookie-Inspektion erfordern. Das ist die Aufgabe von jemand anderem.

Traefik ist der container-bewusste Einstiegspunkt in Ihrer Infrastruktur. When you run a Multi-Tenant-Docker-Stack, Traefiks Wert liegt darin, dass es Container-Labels liest und entsprechend routet – keine manuellen Konfigurationsdateien, die nicht mehr synchron sind, wenn Container neu starten oder skalieren. Es verwaltet TLS-Zertifikate von Let's Encrypt für interne oder Staging-Domains, entfernt oder schreibt Header um und erzwingt Middleware-Ketten wie IP-Allowlisting oder Basic Auth. Traefik cached nicht. Es komprimiert unter Last nicht gut. Seine Aufgabe ist Routing und Middleware, und diese Aufgabe erledigt es effizient.

Varnish ist der HTTP-Accelerator. Er sitzt zwischen dem Einstiegspunkt und dem Anwendungs-Host, um Read-Traffic zu absorbieren. Eine gut abgestimmte Varnish-Instanz kann Zehntausende von Anfragen pro Sekunde aus dem Speicher bedienen und vollständige HTML-Seiten in unter einer Millisekunde zurückgeben. Seine VCL (Varnish Configuration Language) gibt Ihnen präzise Kontrolle darüber, was gecacht wird, wie lange und unter welchen Bedingungen – weit granularer als Cloudflares Cache-Regeln. Das ist wichtig für authentifizierte Anwendungen, bei denen einige Antworten pro Benutzer cachebar sind, oder für Seiten, die je nach Query-Parametern unterschiedliche TTLs tragen sollen.

Nginxspielt in diesem Stack die Rolle, für die es ursprünglich konzipiert wurde: Dateien ausliefern und an einen vorgelagerten Anwendungsprozess weiterleiten. CloudPanel verwendet Nginx als verwalteten Webserver, was bedeutet, dass jeder vhost eine automatisch generierte Konfiguration hat, die PHP-FPM, statisches Asset-Serving und gzip-Komprimierung übernimmt. Diese Konfiguration wird von CloudPanel verwaltet und sollte nicht direkt bearbeitet werden. Nginx ist hier keine entscheidungsnehmende Schicht – es empfängt eine Anfrage, leitet sie an die Anwendung weiter und gibt die Antwort zurück.

Traefik-Konfiguration: Container-Labels

Das Traefik-Routing für diesen Stack wird vollständig über Docker Compose Labels konfiguriert. Die wichtigste Erkenntnis beim Einsatz von Varnish hinter Traefik ist, dass Traefik an den Varnish-Container weiterleiten sollte, nicht direkt an Nginx. Hier werden die meisten Leute verwirrt: the standard Traefik examples zeigen ein Direct-to-App-Muster, und das Einbetten von Varnish in diesen Fluss ist nicht gut dokumentiert.

services:
  varnish:
    image: varnish:7.5
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`www.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls=true"
      - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
      - "traefik.http.services.myapp.loadbalancer.server.port=80"
      - "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Port=443"
      - "traefik.http.routers.myapp.middlewares=myapp-proto"
    networks:
      - traefik-public
      - internal

Der X-Forwarded-Proto-Header ist kritisch. Ohne ihn sieht Ihre Anwendung jede Anfrage als HTTP – weil die Kommunikation von Varnish zu Nginx über einfaches HTTP im internen Docker-Netzwerk läuft – was in jeder Anwendung, die HTTPS erzwingt, Redirect-Schleifen verursacht. Traefik setzt den Header, bevor die Anfrage das interne Netzwerk betritt, Varnish leitet ihn unverändert weiter, und Nginx leitet ihn an die Anwendung weiter. Der Begleit-Header X-Forwarded-Port verhindert dieselbe Klasse von Redirect-Schleifen in Anwendungen, die auch den Port untersuchen.

Eine zweite Überlegung: Traefiks passHostHeader-Einstellung ist standardmäßig true, was bedeutet, dass Varnish den ursprünglichen Host-Header erhält. Das ist gewünscht – Varnishs VCL kann ihn für das Cache-Keying verwenden, und Nginx verwendet ihn für die vhost-Auswahl. Wenn Sie dies überschreiben, erhalten sowohl Varnish als auch Nginx den falschen Host und scheitern stillschweigend auf eine Weise, die schwer nachzuverfolgen ist.

Varnish VCL: Cache-Logik

Das folgende VCL ist ein funktionierender Ausgangspunkt für eine WordPress- oder PHP-Anwendung hinter CloudPanel. Es deckt die üblichen Fälle ab: Bypass für eingeloggte Benutzer und nicht-idempotente Anfragen, erweitertes Caching für statische Assets, Normalisierung von Tracking-Parametern und eine Grace-Period zur Vermeidung von Cache-Stampedes.

vcl 4.1;

backend default {
    .host = "nginx";  # service name in docker-compose
    .port = "80";
    .connect_timeout = 5s;
    .first_byte_timeout = 30s;
    .between_bytes_timeout = 10s;
}

sub vcl_recv {
    # Admin- und authentifizierte Anfragen durchleiten
    if (req.url ~ "^/wp-(admin|login|cron|json)" ||
        req.http.Cookie ~ "wordpress_logged_in_" ||
        req.http.Cookie ~ "woocommerce_items_in_cart") {
        return(pass);
    }

    # Nur GET und HEAD cachen
    if (req.method != "GET" && req.method != "HEAD") {
        return(pass);
    }

    # Tracking-Parameter aus dem Cache-Key normalisieren
    if (req.url ~ "(?|&)(utm_source|utm_medium|utm_campaign|fbclid|gclid)=") {
        set req.url = regsuball(req.url,
            "&(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+", "");
        set req.url = regsuball(req.url,
            "?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+&", "?");
        set req.url = regsub(req.url,
            "?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+$", "");
    }

    # Cookies für statische Assets entfernen – erlaubt Caching
    if (req.url ~ ".(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(?.*)?$") {
        unset req.http.Cookie;
    }

    return(hash);
}

sub vcl_backend_response {
    # Statische Assets für 7 Tage cachen
    if (bereq.url ~ ".(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(?.*)?$") {
        set beresp.ttl = 7d;
        set beresp.grace = 24h;
        unset beresp.http.Set-Cookie;
        return(deliver);
    }

    # HTML-Seiten für 5 Minuten mit 60-sekündiger Grace-Period cachen
    if (beresp.status == 200 && beresp.http.Content-Type ~ "text/html") {
        set beresp.ttl = 300s;
        set beresp.grace = 60s;
    }

    # Fehlerantworten nie cachen
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
    }

    return(deliver);
}

sub vcl_deliver {
    # Cache-Status für Debugging sichtbar machen
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }

    # Server-Identitäts-Header entfernen
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    return(deliver);
}

Die Normalisierung von Tracking-Parametern in vcl_recv ist leicht zu übersehen und teuer zu vernachlässigen. Cloudflare entfernt einige Parameter an der Edge, aber UTM-Werte und fbclid-Tokens erreichen Varnish mit der ursprünglichen URL intakt. Ohne Normalisierung erzeugt jeder Facebook-Anzeigenklick einen einzigartigen Cache-Eintrag für das, was aus Anwendungsperspektive dieselbe Seite ist. Über eine Kampagnenperiode fragmentiert dies den Varnish-Cache in Tausende von kalten Einträgen und eliminiert den Hit-Ratio-Vorteil vollständig.

Die grace-Einstellung in vcl_backend_response ist der andere Mechanismus, der es wert ist, im Detail verstanden zu werden. Wenn die TTL eines gecachten Objekts abläuft, würde die nächste Anfrage normalerweise warten, bis Nginx eine frische Antwort zurückgibt, bevor Varnish irgendetwas liefert. Bei einem Traffic-Spike – einem Newsletter-Versand, einer Social-Media-Erwähnung – treffen mehrere Anfragen gleichzeitig für den abgelaufenen Eintrag ein. Ohne Grace leitet jede Anfrage gleichzeitig zu Nginx weiter. Mit einer 60-sekündigen Grace-Period bedient Varnish das veraltete Objekt an alle bis auf eine dieser Anfragen, während ein einzelner Hintergrund-Fetch den Cache revalidiert. Das Ergebnis ist, dass Nginx zum Zeitpunkt des Ablaufs eine Anfrage statt Hunderten verarbeitet.

Performance-Implikationen

Die Performance-Geschichte dieses Stacks ist nicht einheitlich. Es gibt Stellen, wo er glänzt, und Stellen, wo er messbaren Overhead hinzufügt.

Wo er gut abschneidet: Jede cachefähige Seite, die aus dem Varnish-Speicher bedient wird, gibt auf Traefik-Ebene in unter 2ms zurück. Eine WordPress-Homepage, die 400ms zur PHP-Generierung benötigt, wird bei jeder nachfolgenden Anfrage bis zum TTL-Ablauf in 1–2ms bedient. Die Durchsatzverbesserung ist nicht inkrementell – sie beträgt eine Größenordnung. Statisches Asset-Serving über Nginx ist ohnehin schnell, und Cloudflare cached diese Assets ohnehin an der Edge, sodass Nginx sie in der Produktion selten verarbeitet.

Wo er Overhead hinzufügt: Cache-Misses traversieren jetzt vier Hops statt einem. Eine PHP-Anfrage, die nicht gecacht werden kann – eine eingeloggte Benutzersession, ein POST-Submit, eine WooCommerce-Warenkorbseite – passiert Traefik, Varnish und Nginx, bevor sie PHP-FPM erreicht. Jeder Docker-Netzwerk-Hop auf demselben Host fügt etwa 0,1–0,3ms Latenz hinzu. Für eine 50ms-PHP-Antwort ist das akzeptabel. Für einen API-Endpunkt, der in unter 5ms antworten soll, nicht. In diesem Fall ist der richtige Ansatz, den API-Pfad direkt zum Anwendungscontainer über eine separate Traefik-Router-Regel zu routen und Varnish vollständig zu umgehen.

Die Speichergröße ist wichtiger als die meisten Parameter: Varnish allokiert seinen Objekt-Store aus dem RAM. Die Standardkonfiguration verwendet 256MB. Für eine Website mit vielen einzigartigen URLs füllt sich dieser schnell, die Eviction-Rate steigt und das Cache-Hit-Ratio bleibt unabhängig von TTL-Einstellungen niedrig. Der erste Performance-Hebel nach dem Deployment ist die Erhöhung der Allokation – das -s malloc,2G-Start-Flag oder die VARNISH_SIZE=2G-Umgebungsvariable in Docker – und die Überwachung des Ergebnisses mit varnishstat -1 | grep hit.

SSL-Terminierung: TLS terminiert bei Cloudflare. In einem Cloudflare-fronted Setup laufen die Segmente Traefik-zu-Varnish und Varnish-zu-Nginx über einfaches HTTP im internen Docker-Bridge-Netzwerk. Das ist beabsichtigt. TLS zur internen Container-Kommunikation auf demselben Host hinzuzufügen erzeugt CPU-Overhead ohne eine sinnvolle Sicherheitsverbesserung – das Bedrohungsmodell für Docker-Bridge-Traffic ist grundlegend anders als öffentlicher Internet-Traffic. Wenn Ihr Bedrohungsmodell verschlüsselte Inter-Container-Kommunikation erfordert, ist das eine andere Architektur-Diskussion.

Wo dieser Stack sinnvoll ist

Dies ist keine allgemeine Empfehlung. Für eine einzelne containerisierte Anwendung ohne Caching-Anforderungen und ohne CloudPanel-Einschränkung ist Traefik-Routing direkt zur App einfacher und richtig. Der Vier-Schichten-Stack verdient seine Komplexität in spezifischen Szenarien: CloudPanel-verwalteten Umgebungen, in denen Nginx nicht verhandelbar ist, CMS-gestützten Sites, bei denen Full-Page-Caching Durchsatzverbesserungen um eine Größenordnung bietet, oder multi-service deployments wo Traefik bereits die Routing-Schicht für ein Dutzend Container ist und das Hinzufügen von Varnish für einen davon inkrementelle Kosten sind, kein neues System.

Die Aufgabentrennung ist das, was die Komplexität im Laufe der Zeit handhabbar macht. Die Traefik-Konfiguration lebt in Docker Compose Labels. Die Varnish-Logik lebt in VCL-Dateien, die versionskontrolliert neben dem Compose-Stack liegen. Die Nginx-Konfiguration gehört CloudPanel. Keines dieser Systeme greift in den Bereich der anderen ein. Wenn sich etwas unerwartet verhält, ist der Diagnosepfad klar: Der X-Cache-Header teilt Ihnen mit, ob Varnish die Antwort bedient hat; Traefiks Zugriffsprotokoll zeichnet auf, welche Middleware ausgeführt wurde und was der Upstream empfangen hat; Nginx-Fehlerprotokoll zeichnet auf, was PHP-FPM gemeldet hat. Jede Schicht erzeugt ihr eigenes beobachtbares Signal, und Fehler akkumulieren sich nicht stillschweigend über Schichten hinweg.

Was wie Redundanz aussieht, ist in der Praxis eine klare Grenze zwischen Routing, Caching, Anwendungs-Serving und Compute – vier Belange, die unabhängig skalieren und unabhängig scheitern. Diese Grenze ist die zusätzliche Konfigurationsfläche wert.

Verwandte Beiträge

Weitere Artikel