Empilhando proxies reversos: uma arquitetura para produção
Quatro proxies reversos na frente de uma única aplicação parecem, à primeira vista, um sistema que precisa de simplificação. A maioria dos tutoriais sobre infraestrutura web em produção opta por uma única camada — Nginx, Caddy ou Traefik — e trata qualquer coisa a mais como complexidade excessiva. Mas, na prática, cada uma dessas ferramentas resolve um problema distinto, e quando você entende a divisão de responsabilidades, a redundância aparente se resolve em algo coerente.
Esta é a arquitetura que executamos para várias implantações de clientes: Cloudflare na borda, Traefik como ponto de entrada que reconhece os contêineres, Varnish para cache HTTP, Nginx como host da aplicação e a própria aplicação no final da cadeia. O fluxo de requisições é:
Cloudflare → Traefik → Varnish → Nginx → Application
O que segue explica o que cada camada contribui, onde ficam os limites e quais são as implicações de desempenho — incluindo os pontos onde esta stack gera custos adicionais que uma configuração mais simples não criaria.
O que cada camada realmente faz
Cloudflare opera na borda da internet pública. Absorve tráfego DDoS, aplica regras WAF, encerra TLS em PoPs distribuídos globalmente e armazena em cache recursos estáticos próximos aos usuários finais. O que ele não faz bem é tomar decisões inteligentes de roteamento sobre seus contêineres ou armazenar em cache respostas de aplicação que requerem inspeção de cookies. Isso é trabalho de outra ferramenta.
Traefik é o ponto de entrada que reconhece os contêineres dentro da sua infraestrutura. Quando você executa uma stack Docker com múltiplos clientes, o valor do Traefik está em ler os rótulos dos contêineres e rotear as requisições de acordo com eles — sem arquivos de configuração manuais que ficam desatualizados quando os contêineres reiniciam ou escalam. Ele gerencia certificados TLS do Let's Encrypt para domínios internos ou de staging, remove ou reescreve cabeçalhos e aplica cadeias de middleware como listas de IPs permitidos ou autenticação básica. Traefik não faz cache. Não comprime bem sob carga. Seu trabalho é roteamento e middleware, e ele faz esse trabalho de forma eficiente.
Varnish é o acelerador HTTP. Ele fica entre o ponto de entrada e o host da aplicação especificamente para absorver tráfego de leitura. Uma instância Varnish bem ajustada pode servir dezenas de milhares de requisições por segundo da memória, retornando páginas HTML completas em menos de um milissegundo. Sua VCL (Varnish Configuration Language) oferece controle preciso sobre o que é armazenado em cache, por quanto tempo e em quais condições — muito mais granular do que qualquer regra de cache do Cloudflare oferece. Isso é importante para aplicações autenticadas onde algumas respostas poderiam ser armazenadas em cache por usuário com um cálculo de hash personalizado (o exemplo abaixo não faz isso; encaminha o tráfego de usuários autenticados sem usar o cache), ou para páginas que devem ter TTLs diferentes dependendo dos parâmetros de consulta.
Nginx, nesta stack, desempenha o papel para o qual foi originalmente projetado: servir arquivos e fazer proxy para um processo de aplicação upstream. CloudPanel usa o Nginx como seu servidor web gerenciado, o que significa que cada vhost tem uma configuração gerada automaticamente que lida com PHP-FPM, fornecimento de recursos estáticos e compressão gzip. Essa configuração é mantida pelo CloudPanel e não deve ser editada diretamente. O Nginx aqui não é uma camada de tomada de decisões — ele recebe uma requisição, faz proxy para a aplicação e retorna a resposta.
Configuração do Traefik: rótulos de contêineres
O roteamento do Traefik para esta stack é configurado inteiramente pelos rótulos do Docker Compose. O ponto principal ao colocar o Varnish atrás do Traefik é que o Traefik deve encaminhar para o contêiner Varnish, não diretamente para o Nginx. É aqui que a maioria das pessoas se confunde: os exemplos padrão do Traefik mostram um padrão direto para o app, e inserir o Varnish nesse fluxo não está bem documentado.
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
O cabeçalho X-Forwarded-Proto é crítico. Sem ele, sua aplicação vê cada requisição como HTTP — porque a comunicação entre Varnish e Nginx usa HTTP sem criptografia na rede interna do Docker — o que causa loops de redirecionamento em qualquer aplicação que exija HTTPS. O Traefik adiciona o cabeçalho antes que a requisição entre na rede interna, o Varnish o repassa sem alterações e o Nginx o encaminha para a aplicação. O cabeçalho complementar X-Forwarded-Port previne a mesma classe de loop de redirecionamento em aplicações que também inspecionam a porta.
Uma segunda consideração: a configuração passHostHeader do Traefik tem como padrão true, o que significa que o Varnish recebe o cabeçalho Host original. É isso que você quer — a VCL do Varnish pode usá-lo para definição das chaves de cache, e o Nginx o usa para seleção de vhost. Se você sobrescrever isso, tanto o Varnish quanto o Nginx receberão o host errado e falharão silenciosamente de formas difíceis de rastrear.
VCL do Varnish: lógica de cache
A VCL abaixo é um ponto de partida para uma aplicação WordPress ou PHP atrás do CloudPanel, não uma política de produção finalizada. Ela cobre os casos comuns: desvio do cache para usuários autenticados e requisições não idempotentes, cache estendido para recursos estáticos, normalização de parâmetros de rastreamento e um período de tolerância que permite servir páginas ligeiramente desatualizadas enquanto o Varnish as atualiza. Antes de entrar em produção, exclua também do cache as páginas de carrinho, checkout e conta do WooCommerce e o cookie wp_woocommerce_session_. Como este vcl_backend_response termina com return(deliver), a lógica interna do Varnish que impede o armazenamento em cache de respostas com Set-Cookie ou Cache-Control: private/no-store não é executada; adicione verificações 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);
}
A normalização de parâmetros de rastreamento em vcl_recv é fácil de ignorar e cara de negligenciar. A menos que você configure a reescrita de URLs no Cloudflare, valores UTM e tokens fbclid chegam ao Varnish com a URL original intacta. Sem normalização, cada clique em anúncio do Facebook gera uma entrada de cache única para o que é, da perspectiva da aplicação, a mesma página. Durante um período de campanha, isso fragmenta o cache do Varnish em milhares de entradas sem reutilização e elimina completamente o benefício da taxa de acertos.
A configuração grace em vcl_backend_response é o outro mecanismo que vale entender em detalhes. Quando o TTL de um objeto em cache expira, a próxima requisição normalmente esperaria o Nginx retornar uma resposta atualizada antes do Varnish entregar qualquer coisa. Sob um pico de tráfego — um envio de newsletter, uma menção em redes sociais — múltiplas requisições chegam simultaneamente para a entrada expirada. Sem grace, o Varnish ainda agrupa as requisições de um objeto que pode ser armazenado em cache em uma única busca no Nginx, mas as demais requisições aguardam a conclusão dessa busca. Com um período de tolerância de 60 segundos, o Varnish serve imediatamente o objeto desatualizado para essas requisições enquanto uma única busca em segundo plano atualiza o cache. O resultado é que o Nginx lida com uma requisição em vez de centenas no momento da expiração, e os visitantes não precisam esperar.
Implicações de desempenho
O desempenho desta stack não é uniforme. Há situações em que ela se destaca e outras em que gera um custo adicional mensurável.
Onde tem bom desempenho: qualquer página cacheável servida da memória do Varnish retorna em menos de 2ms no nível do Traefik. Uma página inicial do WordPress que leva 400ms para ser gerada pelo PHP é servida em 1–2ms para cada requisição subsequente até o TTL expirar. A melhoria na capacidade de processamento não é incremental — é uma ordem de grandeza. O fornecimento de recursos estáticos através do Nginx é rápido independentemente, e o Cloudflare armazena esses recursos em cache na borda de qualquer forma, então o Nginx raramente precisa atendê-los em produção.
Onde gera custo adicional: requisições não atendidas pelo cache agora atravessam quatro etapas em vez de uma. Uma requisição PHP que não pode ser armazenada em cache — uma sessão de usuário logado, um envio POST, uma página de carrinho do WooCommerce — passa pelo Traefik, Varnish e Nginx antes de chegar ao PHP-FPM. Cada salto na rede Docker no mesmo host adiciona aproximadamente 0,1–0,3ms de latência. Para uma resposta PHP de 50ms, isso é aceitável. Para um endpoint de API que deve responder em menos de 5ms, não é. Nesse caso, a abordagem correta é rotear o caminho da API diretamente para o contêiner da aplicação via uma regra de roteador Traefik separada, contornando o Varnish completamente.
O dimensionamento de memória importa mais do que a maioria dos parâmetros: o Varnish aloca seu armazenamento de objetos da RAM. A imagem oficial do Docker usa 100 MB por padrão. Para um site com muitas URLs únicas, isso se preenche rapidamente, a taxa de remoção de objetos do cache aumenta e a taxa de acertos do cache permanece baixa independentemente das configurações de TTL. A primeira medida para melhorar o desempenho após a implantação é aumentar a alocação — a opção de inicialização -s malloc,2G, ou a variável de ambiente VARNISH_SIZE=2G no Docker — e monitorar o resultado com varnishstat -1 | grep hit.
Terminação de SSL: a conexão TLS pública termina no Cloudflare. Com o modo SSL “Full (strict)” do Cloudflare, ele se conecta ao Traefik por HTTPS usando o certificado Let’s Encrypt do roteador mostrado acima. Atrás do Traefik, os segmentos entre Traefik e Varnish e entre Varnish e Nginx usam HTTP sem criptografia na rede bridge interna do Docker. Isso é intencional. Em um único host onde você controla todos os contêineres, adicionar TLS à comunicação interna entre eles introduz custo adicional de CPU com um ganho de segurança limitado, porque o tráfego da bridge não sai do host; isso não protegeria contra um contêiner ou host comprometido. Se seu modelo de ameaça requer comunicação criptografada entre contêineres, essa é uma conversa arquitetural diferente.
Quando esta stack faz sentido
Esta não é uma recomendação de uso geral. Para uma única aplicação em contêiner sem requisitos de cache e sem a restrição do CloudPanel, o Traefik roteando diretamente para o app é mais simples e correto. A stack de quatro camadas justifica sua complexidade em cenários específicos: ambientes gerenciados pelo CloudPanel onde o Nginx não é negociável, sites baseados em CMS onde o cache de página completa proporciona melhorias na capacidade de processamento de ordem de grandeza, ou implantações com múltiplos serviços onde o Traefik já é a camada de roteamento para uma dúzia de contêineres e adicionar Varnish para um deles é custo incremental, não um novo sistema.
A separação de responsabilidades é o que torna a complexidade gerenciável ao longo do tempo. A configuração do Traefik fica nos rótulos do Docker Compose. A lógica do Varnish fica em arquivos VCL com controle de versão junto com a stack do Docker Compose. A configuração do Nginx é de propriedade do CloudPanel. Nenhum desses sistemas interfere nas responsabilidades dos demais. Quando algo se comporta de forma inesperada, o caminho de diagnóstico é claro: o cabeçalho X-Cache diz se o Varnish serviu a resposta; o log de acesso do Traefik registra o roteador, o serviço de backend e os códigos de status retornados pelo upstream e enviados ao cliente; o log de erros do Nginx registra o que o PHP-FPM informou. Cada camada produz seu próprio sinal observável, e as falhas não se acumulam silenciosamente entre as camadas.
O que parece redundância é, na prática, um limite claro entre proteção na borda, roteamento, cache e disponibilização da aplicação — quatro responsabilidades que crescem de forma independente e falham independentemente. Essa separação justifica os pontos adicionais de configuração.
Artigos relacionados
- Implantando aplicações React em produção: configuração completa com Docker e Traefik como proxy reverso
- Guia completo de solução de problemas do n8n auto-hospedado 2025: corrigindo problemas de tamanho de dados de execução e webhooks com Traefik
- Construindo uma stack de desenvolvimento com múltiplos clientes com Docker: configuração completa para implantações escaláveis de clientes
- Otimização de desempenho do CloudPanel: maximizando o desempenho do servidor Hetzner Cloud para entrega ultrarrápida de sites