tva
← Artículos

Proxies inversos en cadena: una arquitectura para producción

A primera vista, colocar cuatro proxies inversos delante de una sola aplicación parece propio de un sistema que necesita simplificarse. La mayoría de los tutoriales sobre infraestructura web en producción se limitan a una capa —Nginx, Caddy o Traefik— y consideran que añadir más es complicar la arquitectura sin necesidad. Pero, en realidad, cada una de estas herramientas resuelve un problema distinto y, al entender el reparto de funciones, la aparente redundancia se convierte en una arquitectura coherente.

Esta es la arquitectura que utilizamos en varios despliegues para clientes: Cloudflare en el borde de la red, Traefik como punto de entrada que reconoce los contenedores, Varnish para la caché HTTP, Nginx como servidor de la aplicación y la propia aplicación al final de la cadena. El flujo de las solicitudes es el siguiente:

Cloudflare → Traefik → Varnish → Nginx → Application

A continuación explicamos qué aporta cada capa, dónde están sus límites y cuáles son sus implicaciones para el rendimiento, incluidos los puntos en los que este stack añade una carga que una configuración más sencilla no tendría.

Qué hace realmente cada capa

Cloudflare opera en el borde de la internet pública. Absorbe el tráfico DDoS, aplica reglas WAF, termina las conexiones TLS en puntos de presencia distribuidos por todo el mundo y almacena en caché los recursos estáticos cerca de los usuarios finales. Lo que no hace bien es tomar decisiones inteligentes de enrutamiento sobre los contenedores ni almacenar en caché respuestas de la aplicación que requieren inspeccionar cookies. Esa tarea corresponde a otra capa.

Traefik es el punto de entrada dentro de su infraestructura que reconoce los contenedores. Al ejecutar un stack Docker multiinquilino, su valor reside en que lee las etiquetas de los contenedores y enruta en consecuencia: no hay archivos de configuración manuales que queden desactualizados cuando los contenedores se reinician o escalan. Gestiona los certificados TLS de Let's Encrypt para dominios internos o de pruebas, elimina o reescribe cabeceras y aplica cadenas de middleware, como listas de direcciones IP permitidas o autenticación básica. Traefik no almacena en caché. Tampoco comprime bien bajo carga. Su función es el enrutamiento y el middleware, y la cumple de forma eficiente.

Varnish es el acelerador HTTP. Se sitúa entre el punto de entrada y el servidor de la aplicación específicamente para absorber el tráfico de lectura. Una instancia de Varnish bien ajustada puede atender decenas de miles de solicitudes por segundo desde la memoria y devolver páginas HTML completas en menos de un milisegundo. Su VCL (Varnish Configuration Language) permite controlar con precisión qué se almacena en caché, durante cuánto tiempo y bajo qué condiciones, con mucho más detalle que las reglas de caché de Cloudflare. Esto importa en aplicaciones con autenticación cuyas respuestas podrían almacenarse en caché por usuario con un cálculo de hash personalizado (el ejemplo siguiente no lo hace; omite la caché para el tráfico de usuarios con sesión iniciada), o en páginas que deben tener distintos tiempos de vida (TTL) según los parámetros de consulta.

Nginx cumple en este stack la función para la que se diseñó originalmente: servir archivos y actuar como proxy hacia el proceso de la aplicación situado detrás de él. CloudPanel utiliza Nginx como servidor web gestionado, por lo que cada host virtual tiene una configuración generada automáticamente que se encarga de PHP-FPM, de servir recursos estáticos y de la compresión gzip. CloudPanel mantiene esa configuración y no debe editarse directamente. Aquí Nginx no es una capa de toma de decisiones: recibe una solicitud, la envía a la aplicación y devuelve la respuesta.

Configuración de Traefik: etiquetas de contenedores

El enrutamiento de Traefik para este stack se configura íntegramente mediante etiquetas de Docker Compose. La clave al colocar Varnish detrás de Traefik es que Traefik debe reenviar las solicitudes al contenedor de Varnish, no directamente a Nginx. Aquí es donde suele surgir la confusión: los ejemplos habituales de Traefik muestran un patrón de acceso directo a la aplicación, y la incorporación de Varnish a ese flujo está poco documentada.

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

La cabecera X-Forwarded-Proto es fundamental. Sin ella, la aplicación interpreta cada solicitud como HTTP, porque la comunicación entre Varnish y Nginx utiliza HTTP sin cifrar en la red interna de Docker. Esto provoca bucles de redirección en cualquier aplicación que imponga HTTPS. Traefik establece la cabecera antes de que la solicitud entre en la red interna, Varnish la transmite sin cambios y Nginx la reenvía a la aplicación. La cabecera complementaria X-Forwarded-Port evita el mismo tipo de bucle de redirección en las aplicaciones que también comprueban el puerto.

Hay otra consideración: la opción passHostHeader de Traefik tiene el valor true de forma predeterminada, por lo que Varnish recibe la cabecera Host original. Ese es el comportamiento deseado: el VCL de Varnish puede utilizarla para definir la clave de caché y Nginx la utiliza para seleccionar el host virtual. Si se modifica esta opción, tanto Varnish como Nginx recibirán un host incorrecto y fallarán sin indicarlo, de formas difíciles de rastrear.

VCL de Varnish: lógica de caché

El VCL siguiente es un punto de partida para una aplicación de WordPress o PHP detrás de CloudPanel, no una política de producción terminada. Cubre los casos habituales: omitir la caché para usuarios con sesión iniciada y solicitudes no idempotentes, prolongar el almacenamiento en caché de recursos estáticos, normalizar los parámetros de seguimiento y establecer un período de gracia que permite servir páginas ligeramente caducadas mientras Varnish las actualiza. Antes de pasar a producción, excluya también de la caché las páginas de carrito, proceso de pago y cuenta de WooCommerce, así como la cookie wp_woocommerce_session_. Como este vcl_backend_response termina con return(deliver), no se ejecuta la lógica integrada de Varnish que impide almacenar en caché respuestas con Set-Cookie o Cache-Control: private/no-store, por lo que debe añadir comprobaciones equivalentes.

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 normalización de los parámetros de seguimiento en vcl_recv es fácil de pasar por alto, y descuidarla sale caro. A menos que configure la reescritura de URL en Cloudflare, los valores UTM y los tokens fbclid llegan a Varnish con la URL original intacta. Sin normalización, cada clic en un anuncio de Facebook genera una entrada de caché única para lo que, desde el punto de vista de la aplicación, es la misma página. Durante una campaña, esto fragmenta la caché de Varnish en miles de entradas sin reutilizar y elimina por completo el beneficio de la tasa de aciertos de caché.

La opción grace de vcl_backend_response es el otro mecanismo que conviene entender en detalle. Cuando vence el TTL de un objeto almacenado en caché, normalmente la siguiente solicitud debe esperar a que Nginx devuelva una respuesta nueva antes de que Varnish entregue nada. Durante un pico de tráfico —el envío de un boletín o una mención en redes sociales—, varias solicitudes llegan simultáneamente para la entrada caducada. Sin un período de gracia, Varnish sigue agrupando las solicitudes de un objeto almacenable en caché en una sola consulta a Nginx, pero las demás esperan a que esta termine. Con un período de gracia de 60 segundos, Varnish entrega inmediatamente el objeto caducado a esas solicitudes mientras una única consulta en segundo plano actualiza la caché. Así, en el momento de la caducidad, Nginx atiende una solicitud en lugar de cientos y los visitantes no tienen que esperar.

Implicaciones para el rendimiento

El rendimiento de este stack no es uniforme. Hay situaciones en las que destaca y otras en las que añade una carga medible.

Dónde funciona bien: cualquier página que pueda almacenarse en caché y se sirva desde la memoria de Varnish se devuelve en menos de 2 ms medidos en Traefik. Una página de inicio de WordPress que tarda 400 ms en generarse mediante PHP se sirve en 1–2 ms para cada solicitud posterior hasta que vence el TTL. La mejora de la tasa de procesamiento no es incremental: es de un orden de magnitud. Nginx sirve los recursos estáticos rápidamente en cualquier caso, y Cloudflare los almacena en caché en el borde de la red, por lo que Nginx rara vez los atiende en producción.

Dónde añade carga: los fallos de caché ahora recorren cuatro saltos en lugar de uno. Una solicitud PHP que no pueda almacenarse en caché —una sesión de un usuario autenticado, un envío POST o una página de carrito de WooCommerce— pasa por Traefik, Varnish y Nginx antes de llegar a PHP-FPM. Cada salto de red de Docker en el mismo servidor añade aproximadamente entre 0,1 y 0,3 ms de latencia. Para una respuesta PHP de 50 ms, es aceptable. Para un endpoint de API que debería responder en menos de 5 ms, no lo es. En ese caso, el enfoque correcto es enrutar la ruta de la API directamente al contenedor de la aplicación mediante una regla de enrutamiento independiente de Traefik, omitiendo Varnish por completo.

El dimensionamiento de la memoria importa más que la mayoría de los parámetros: Varnish reserva RAM para almacenar sus objetos. La imagen oficial de Docker utiliza 100 MB de forma predeterminada. En un sitio con muchas URL únicas, ese espacio se llena rápidamente, aumenta la tasa de expulsión de objetos y la tasa de aciertos de caché sigue siendo baja, independientemente de los ajustes de TTL. La primera medida para mejorar el rendimiento tras el despliegue es aumentar la asignación —con la opción de inicio -s malloc,2G o la variable de entorno VARNISH_SIZE=2G en Docker— y monitorear el resultado con varnishstat -1 | grep hit.

Terminación SSL: El TLS público termina en Cloudflare. Con el modo SSL «Full (strict)» de Cloudflare, este se conecta después a Traefik por HTTPS utilizando el certificado de Let’s Encrypt del router mostrado arriba. Detrás de Traefik, los tramos de Traefik a Varnish y de Varnish a Nginx utilizan HTTP sin cifrar en la red de puente interna de Docker. Es una decisión deliberada. En un solo servidor donde usted controla todos los contenedores, añadir TLS a la comunicación interna entre contenedores consume CPU con una mejora limitada de la seguridad, porque el tráfico del puente no sale del servidor; no lo protegería frente a un contenedor o servidor comprometido. Si su modelo de amenazas exige cifrar la comunicación entre contenedores, eso requiere otro planteamiento arquitectónico.

Dónde tiene sentido este stack

Esta no es una recomendación de uso general. Para una sola aplicación en contenedores, sin requisitos de caché ni la obligación de utilizar CloudPanel, enrutar directamente desde Traefik a la aplicación es más sencillo y correcto. El stack de cuatro capas justifica su complejidad en situaciones concretas: entornos gestionados con CloudPanel en los que Nginx es obligatorio, sitios basados en un CMS donde la caché de páginas completas mejora la tasa de procesamiento en un orden de magnitud, o despliegues con varios servicios en los que Traefik ya enruta una docena de contenedores y añadir Varnish para uno de ellos supone un costo adicional, no un sistema nuevo.

La separación de responsabilidades permite gestionar esa complejidad a lo largo del tiempo. La configuración de Traefik reside en las etiquetas de Docker Compose. La lógica de Varnish reside en archivos VCL bajo control de versiones junto al stack de Compose. CloudPanel se encarga de la configuración de Nginx. Ninguno de estos sistemas invade el ámbito de los demás. Cuando algo se comporta de forma inesperada, el recorrido del diagnóstico está claro: la cabecera X-Cache indica si Varnish sirvió la respuesta; el registro de acceso de Traefik recoge el router, el servicio de backend y los códigos de estado devueltos por el servidor de destino y enviados al cliente; el registro de errores de Nginx recoge lo que notificó PHP-FPM. Cada capa genera su propia señal observable y los fallos no se acumulan silenciosamente entre capas.

Lo que parece redundancia es, en la práctica, un límite claro entre protección en el borde de la red, enrutamiento, caché y servicio de la aplicación: cuatro responsabilidades que escalan y fallan de forma independiente. Ese límite justifica la configuración adicional.

Artículos relacionados

Artículos relacionados