tva
← Analyses

Empiler des proxys inverses : une architecture de production

Quatre proxys inverses devant une seule application ressemblent, au premier abord, à un système qui aurait besoin d’être simplifié. La plupart des tutoriels sur l’infrastructure web en production s’arrêtent à une seule couche — Nginx, Caddy ou Traefik — et considèrent tout ce qui va au-delà comme une complexité excessive. Mais en réalité, chacun de ces outils résout un problème distinct, et lorsqu’on comprend la division du travail, la redondance apparente se transforme en quelque chose de cohérent.

C’est l’architecture que nous utilisons pour plusieurs déploiements clients : Cloudflare en périphérie, Traefik comme point d’entrée qui tient compte des conteneurs, Varnish pour le cache HTTP, Nginx comme hôte applicatif, et l’application elle-même en bout de chaîne. Le flux de requêtes est le suivant :

Cloudflare → Traefik → Varnish → Nginx → Application

Ce qui suit explique ce que chaque couche apporte, où se situent les frontières, et quelles sont les implications sur les performances — y compris les endroits où cette pile crée une surcharge qu’une configuration plus simple n’aurait pas.

Ce que fait réellement chaque couche

Cloudflare opère en périphérie de l’Internet public. Il absorbe le trafic DDoS, applique les règles WAF, assure la terminaison TLS sur des points de présence (PoP) répartis dans le monde entier et met en cache les ressources statiques proches des utilisateurs finaux. Ce qu’il ne fait pas bien, c’est prendre des décisions de routage intelligentes concernant vos conteneurs, ou mettre en cache des réponses applicatives qui nécessitent l’inspection des cookies. C’est le rôle d’une autre couche.

Traefik est le point d’entrée qui tient compte des conteneurs à l’intérieur de votre infrastructure. Lorsque vous exploitez une pile Docker mutualisée entre plusieurs clients, la valeur de Traefik est qu’il lit les labels des conteneurs et route en conséquence — pas de fichiers de configuration manuels qui se désynchronisent quand les conteneurs redémarrent ou montent en charge. Il gère les certificats TLS via Let's Encrypt pour les domaines internes ou de préproduction, supprime ou réécrit les en-têtes, et applique des chaînes de middleware comme la liste d’adresses IP autorisées ou l’authentification basique. Traefik ne met pas en cache. Il ne compresse pas bien sous charge. Son rôle est le routage et le middleware, et il s’en acquitte efficacement.

Varnish est l’accélérateur HTTP. Il se situe entre le point d’entrée et l’hôte applicatif précisément pour absorber le trafic en lecture. Une instance Varnish bien configurée peut servir des dizaines de milliers de requêtes par seconde depuis la mémoire, retournant des pages HTML complètes en moins d’une milliseconde. Son VCL (Varnish Configuration Language) vous donne un contrôle précis sur ce qui est mis en cache, pendant combien de temps et dans quelles conditions — bien plus fin que tout ce que les règles de cache de Cloudflare offrent. Ce contrôle est important pour les applications authentifiées où certaines réponses pourraient être mises en cache par utilisateur avec un calcul de hachage personnalisé (l’exemple ci-dessous ne le fait pas ; il transmet le trafic des utilisateurs connectés sans mise en cache), ou pour les pages qui doivent avoir des TTL différents selon les paramètres de requête.

Nginx, dans cette pile, joue le rôle pour lequel il a été conçu à l’origine : servir des fichiers et transmettre des requêtes à un processus applicatif en amont. CloudPanel utilise Nginx comme son serveur web géré, ce qui signifie que chaque hôte virtuel (vhost) dispose d’une configuration auto-générée qui gère PHP-FPM, la diffusion des ressources statiques et la compression gzip. Cette configuration est maintenue par CloudPanel et ne devrait pas être modifiée directement. Nginx ici n’est pas une couche de prise de décision — il reçoit une requête, la transmet à l’application et retourne la réponse.

Configuration Traefik : labels de conteneur

Le routage Traefik pour cette pile est configuré entièrement via des labels Docker Compose. Le point essentiel lorsqu’on place Varnish derrière Traefik est que Traefik doit transférer vers le conteneur Varnish, et non directement vers Nginx. C’est là que la plupart des gens se perdent : les exemples Traefik standards montrent un schéma de routage direct vers l’application, et greffer Varnish dans ce flux n’est pas bien documenté.

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

L’en-tête X-Forwarded-Proto est critique. Sans lui, votre application voit chaque requête comme HTTP — parce que la communication Varnish-vers-Nginx s’effectue en HTTP simple sur le réseau Docker interne — ce qui provoque des boucles de redirection dans toute application imposant HTTPS. Traefik définit l’en-tête avant que la requête n’entre dans le réseau interne, Varnish le transmet intact, et Nginx le transmet à l’application. L’en-tête complémentaire X-Forwarded-Port évite la même classe de boucle de redirection dans les applications qui inspectent également le port.

Une deuxième considération : le paramètre passHostHeader de Traefik vaut true par défaut, ce qui signifie que Varnish reçoit l’en-tête Host original. C’est ce que vous souhaitez — le VCL de Varnish peut l’utiliser pour la construction des clés de cache, et Nginx l’utilise pour la sélection de l’hôte virtuel. Si vous le remplacez, Varnish et Nginx recevront tous deux le mauvais nom d’hôte et échoueront silencieusement de manières difficiles à tracer.

VCL Varnish : logique de cache

Le VCL ci-dessous est un point de départ pour une application WordPress ou PHP derrière CloudPanel, et non une politique de production aboutie. Il couvre les cas courants : contournement du cache pour les utilisateurs connectés et les requêtes non idempotentes, mise en cache prolongée des ressources statiques, normalisation des paramètres de suivi et période de grâce permettant de servir des pages légèrement périmées pendant que Varnish les actualise. Avant la mise en production, excluez aussi du cache les pages de panier, de paiement et de compte de WooCommerce ainsi que le cookie wp_woocommerce_session_. Comme ce vcl_backend_response se termine par return(deliver), la logique intégrée de Varnish qui rend non cachables les réponses contenant Set-Cookie ou Cache-Control: private/no-store n’est pas exécutée ; ajoutez donc des vérifications équivalentes.

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 {
    # Pass admin and authenticated requests
    if (req.url ~ "^/wp-(admin|login|cron|json)" ||
        req.http.Cookie ~ "wordpress_logged_in_" ||
        req.http.Cookie ~ "woocommerce_items_in_cart") {
        return(pass);
    }

    # Only cache GET and HEAD
    if (req.method != "GET" && req.method != "HEAD") {
        return(pass);
    }

    # Normalise tracking parameters out of the cache key
    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)=[^&]+$", "");
    }

    # Remove cookies for static assets — allows caching
    if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
        unset req.http.Cookie;
    }

    return(hash);
}

sub vcl_backend_response {
    # Cache static assets for 7 days
    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);
    }

    # Cache HTML pages for 5 minutes with a 60-second grace period
    if (beresp.status == 200 && beresp.http.Content-Type ~ "text/html") {
        set beresp.ttl = 300s;
        set beresp.grace = 60s;
    }

    # Never cache error responses
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
    }

    return(deliver);
}

sub vcl_deliver {
    # Expose cache status for debugging
    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";
    }

    # Remove server identity headers
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    return(deliver);
}

La normalisation des paramètres de suivi dans vcl_recv est facile à négliger et coûteuse si on l’omet. À moins de configurer la réécriture d’URL chez Cloudflare, les valeurs UTM et les jetons fbclid arrivent chez Varnish avec l’URL originale intacte. Sans normalisation, chaque clic sur une publicité Facebook génère une entrée de cache unique pour ce qui est, du point de vue de l’application, la même page. Sur une période de campagne, cela fragmente le cache Varnish en milliers d’entrées froides et élimine entièrement le bénéfice des réponses servies depuis le cache.

Le paramètre grace dans vcl_backend_response est l’autre mécanisme qui mérite d’être bien compris. Lorsque le TTL d’un objet en cache expire, la prochaine requête attendrait normalement que Nginx retourne une réponse fraîche avant que Varnish ne serve quoi que ce soit. Lors d’un pic de trafic — l’envoi d’une newsletter, une mention sur les réseaux sociaux — plusieurs requêtes arrivent simultanément pour l’entrée expirée. Sans période de grâce, Varnish regroupe toujours les requêtes pour un objet cachable en une seule récupération auprès de Nginx, mais les autres requêtes attendent qu’elle se termine. Avec une période de grâce de 60 secondes, Varnish leur sert immédiatement l’objet périmé pendant qu’une seule récupération en arrière-plan actualise le cache. Le résultat est que Nginx gère une requête au lieu de centaines au moment de l’expiration, et les visiteurs n’ont pas à attendre.

Implications sur les performances

La performance de cette pile n’est pas uniforme. Il y a des endroits où elle excelle et des endroits où elle ajoute une surcharge mesurable.

Là où elle est performante : toute page pouvant être mise en cache servie depuis la mémoire de Varnish retourne en moins de 2 ms au niveau Traefik. Une page d’accueil WordPress qui prend 400 ms à générer depuis PHP est servie en 1–2 ms pour chaque requête ultérieure jusqu’à l’expiration du TTL. L’amélioration du débit n’est pas incrémentale — c’est un ordre de grandeur. La diffusion des ressources statiques via Nginx est rapide dans tous les cas, et Cloudflare met ces ressources en cache en périphérie de toute façon, donc Nginx les gère rarement en production.

Là où elle ajoute une surcharge : les requêtes sans réponse en cache traversent désormais quatre sauts au lieu d’un. Une requête PHP qui ne peut pas être mise en cache — une session d’utilisateur connecté, une soumission POST, une page de panier WooCommerce — passe par Traefik, Varnish et Nginx avant d’atteindre PHP-FPM. Chaque saut réseau Docker sur le même hôte ajoute environ 0,1 à 0,3 ms de latence. Pour une réponse PHP de 50 ms, c’est acceptable. Pour un point de terminaison de l’API qui devrait répondre en moins de 5 ms, ce ne l’est pas. Dans ce cas, la bonne approche est de router le chemin API directement vers le conteneur applicatif via une règle de routeur Traefik séparée, en contournant entièrement Varnish.

Le dimensionnement de la mémoire est plus important que la plupart des paramètres : Varnish alloue son stockage d’objets depuis la RAM. L’image Docker officielle utilise 100 Mo par défaut. Pour un site avec de nombreuses URLs uniques, celui-ci se remplit rapidement, le taux d’éviction augmente, et le taux de réponses servies depuis le cache reste faible quels que soient les paramètres TTL. Le premier levier de performance à actionner après le déploiement est d’augmenter l’allocation — l’option de démarrage -s malloc,2G, ou la variable d’environnement VARNISH_SIZE=2G dans Docker — et de surveiller le résultat avec varnishstat -1 | grep hit.

Terminaison SSL : La connexion TLS publique se termine chez Cloudflare. Avec le mode SSL « Full (strict) » de Cloudflare, celui-ci se connecte ensuite à Traefik en HTTPS en utilisant le certificat Let’s Encrypt du routeur présenté plus haut. Derrière Traefik, les segments Traefik-vers-Varnish et Varnish-vers-Nginx fonctionnent en HTTP simple sur le réseau de pont Docker interne. C’est intentionnel. Sur un hôte unique dont vous contrôlez tous les conteneurs, ajouter TLS à la communication inter-conteneurs interne introduit une surcharge CPU pour un gain de sécurité limité, car le trafic du pont ne quitte pas l’hôte ; cela ne vous protégerait pas contre un conteneur ou un hôte compromis. Si votre modèle de menace requiert une communication inter-conteneurs chiffrée, c’est une autre discussion d’architecture.

Quand cette pile est pertinente

Ce n’est pas une recommandation générale. Pour une seule application conteneurisée sans exigences de cache et sans contrainte CloudPanel, Traefik routant directement vers l’application est plus simple et correct. La pile à quatre couches justifie sa complexité dans des scénarios spécifiques : les environnements gérés par CloudPanel où Nginx n’est pas négociable, les sites adossés à un CMS où la mise en cache de pages complètes procure des améliorations de débit d’un ordre de grandeur, ou les déploiements multi-services où Traefik est déjà la couche de routage pour une douzaine de conteneurs et l’ajout de Varnish pour l’un d’eux représente un coût incrémental, pas un nouveau système.

La séparation des responsabilités est ce qui rend la complexité gérable dans le temps. La configuration Traefik se trouve dans les labels Docker Compose. La logique Varnish se trouve dans des fichiers VCL versionnés aux côtés de la pile Docker Compose. La configuration Nginx est gérée par CloudPanel. Aucun de ces systèmes n’empiète sur le domaine des autres. Quand quelque chose se comporte de manière inattendue, le chemin de diagnostic est clair : l’en-tête X-Cache vous indique si Varnish a servi la réponse ; le journal d’accès de Traefik enregistre le routeur, le service backend et les codes d’état retournés par le serveur en amont et envoyés au client ; le journal d’erreurs de Nginx enregistre ce que PHP-FPM a signalé. Chaque couche produit son propre signal observable, et les défaillances ne s’accumulent pas silencieusement d’une couche à l’autre.

Ce qui ressemble à de la redondance est, en pratique, une frontière claire entre la protection en périphérie, le routage, le cache et le service applicatif — quatre responsabilités dont la montée en charge et les défaillances sont indépendantes les unes des autres. Cette frontière vaut bien le surcroît de configuration.

Ressources connexes

Pour aller plus loin