11 min

Proxy reverso: o que é, como funciona e por que não é um proxy

Gregory Serrao
A diferença entre proxy e proxy reverso, o caminho da requisição com os cabeçalhos reais, e as cinco armadilhas que derrubam a configuração.

Você subiu a aplicação num servidor. Ela roda na porta 3000, você abre a porta no firewall, acessa por http://seu-ip:3000 e funciona. Aí chega a vida real:

  • o cliente quer https://app.suaempresa.com, sem porta e com cadeado;
  • você precisa subir uma segunda aplicação, e a porta 443 é só uma;
  • o /api tem que ir para o backend e o resto para o front;
  • e alguém pergunta quantas requisições por segundo aquele IP aguenta antes de cair.

Cada um desses problemas tem uma solução separada. Só que existe uma única peça que resolve os quatro de uma vez, e ela tem nome: proxy reverso.

Este guia é a explicação que eu queria ter achado em português — com o caminho real da requisição, os cabeçalhos que trafegam de verdade, e as cinco armadilhas que fazem a configuração "correta" quebrar em produção.


A diferença que confunde todo mundo

Se você pesquisou "proxy reverso", provavelmente também pesquisou "proxy reverso vs proxy". É a dúvida número um, e ela existe por um motivo justo: é o mesmo software. O nginx faz os dois. O Apache faz os dois. A diferença não está no programa — está em de que lado você está.

Proxy comum (ou forward proxy) trabalha para o cliente. Você configura o proxy no seu navegador ou na rede da empresa, e ele sai à internet no seu lugar. O site do outro lado vê o IP do proxy, não o seu. É o que a rede corporativa usa para filtrar acesso, e é o que uma VPN faz em espírito.

Proxy reverso trabalha para o servidor. Quem instala é o dono da aplicação. O visitante acha que está falando com um servidor só — e não tem como saber quantas máquinas existem atrás, nem em que linguagem cada uma foi escrita.

A regra de bolso que resolve para sempre:

Quem instalou o proxy é quem ele protege.

Se fui eu, cliente, que configurei, é proxy comum: ele me esconde do servidor. Se foi o dono do site, é proxy reverso: ele esconde o servidor de mim.

Proxy comum Proxy reverso
Trabalha para o cliente o servidor
Quem configura quem navega quem hospeda
Fica escondido o cliente a infraestrutura
O cliente sabe que existe? sim, ele configurou não
Exemplo típico proxy da empresa, VPN nginx na frente da sua API

Como funciona, requisição por requisição

O ponto que a maioria das explicações pula: não existe uma conexão, existem duas. O navegador conversa com o proxy reverso. O proxy reverso abre uma conexão nova, dele, com a sua aplicação. Uma coisa não é a continuação da outra.

O navegador manda algo assim:

GET /api/pedidos HTTP/1.1
Host: app.suaempresa.com
User-Agent: Mozilla/5.0 ...
Cookie: sessao=abc123

O nginx recebe, termina o TLS ali mesmo, e abre uma segunda conexão — em HTTP simples, dentro da rede interna — com a sua aplicação:

GET /api/pedidos HTTP/1.1
Host: app.suaempresa.com
X-Real-IP: 189.40.12.7
X-Forwarded-For: 189.40.12.7
X-Forwarded-Proto: https
Cookie: sessao=abc123

Repare no que mudou: apareceram três cabeçalhos que o navegador nunca mandou. Eles existem porque, da perspectiva da sua aplicação, quem está batendo na porta é o nginx — mesmo IP, sempre, em toda requisição. Sem esses cabeçalhos você perde três informações de uma vez: quem é o visitante, se ele veio por HTTPS, e qual domínio ele digitou.

É por isso que log de aplicação atrás de proxy reverso costuma ficar com o mesmo IP repetido em todas as linhas — e é um problema de configuração, não do proxy.


O que você ganha de verdade

Vou separar o que é benefício real do que costuma ser vendido como benefício e não é.

Um endereço só, uma porta só. Dez aplicações, dez portas internas, e para o mundo só existe a 443. Adicionar a décima primeira aplicação vira um bloco de configuração, não uma negociação de firewall.

TLS em um lugar só. O certificado fica no proxy. Suas aplicações voltam a falar HTTP simples na rede interna e nunca mais precisam saber o que é renovação de certificado. Isso se chama terminação TLS, e sozinho já justifica a peça.

Roteamento por domínio e por caminho. api.site.com vai para um lugar, site.com/blog para outro, e o visitante não percebe emenda.

Buffer para cliente lento. Este é o benefício que quase ninguém explica, e é enorme se sua aplicação tem número fixo de workers — Node com cluster, Rails com Puma, PHP-FPM, Gunicorn. Um visitante em rede ruim leva oito segundos para receber a resposta. Sem proxy, o worker que gerou aquela resposta fica preso oito segundos alimentando um cano lento, sem poder atender mais ninguém. Com nginx na frente, o worker despeja a resposta no nginx em milissegundos, é liberado, e o nginx — que aguenta milhares de conexões ociosas sem suar — cuida de empurrar os bytes no ritmo do coitado. Você acabou de multiplicar sua capacidade sem trocar uma linha da aplicação.

Balanceamento de carga. Várias instâncias atrás do mesmo nome, com o proxy distribuindo e tirando de circulação quem parou de responder.

Um ponto único para as regras. Limite de requisição, bloqueio de IP, cabeçalho de segurança, compressão. Escrito uma vez, valendo para todas as aplicações.

Esconder a topologia. O mundo não descobre que você usa três máquinas, nem qual porta, nem qual framework.

E o que ele não resolve

Ser honesto aqui evita frustração depois:

  • Não deixa sua aplicação mais rápida. Cache ajuda no que é cacheável; consulta lenta continua lenta.
  • Não é autenticação. Dá para pôr uma senha básica na frente, mas isso não substitui o controle de acesso da aplicação.
  • Vira ponto único de falha. Se o proxy cai, tudo cai junto — mesmo com todas as aplicações de pé.
  • Adiciona um salto. É pequeno, quase sempre menos de um milissegundo na rede interna, mas não é zero.

A configuração mínima que realmente funciona

Não é a menor possível — é a menor que não te deixa na mão depois:

# HTTP existe só para mandar todo mundo para o HTTPS
server {
    listen 80;
    server_name app.suaempresa.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name app.suaempresa.com;

    ssl_certificate     /etc/letsencrypt/live/app.suaempresa.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.suaempresa.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;

        # sem estes quatro, a aplicação fica cega
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 5s;
        proxy_read_timeout   60s;
    }
}

Duas observações que economizam tempo:

O return 301 na porta 80 é melhor que qualquer outra forma de redirecionar, porque o nginx responde sem nem tocar na aplicação.

O $proxy_add_x_forwarded_for acrescenta o IP à lista em vez de sobrescrever. Se houver outro proxy na frente — Cloudflare, um balanceador da nuvem — a cadeia inteira é preservada.


As cinco armadilhas

Estas são as que eu vejo quebrarem em produção, na ordem em que aparecem.

1. localhost dentro de container não é o que você pensa

Campeã absoluta. O nginx roda em container, você escreve proxy_pass http://localhost:3000, e o localhost ali dentro é o próprio container do nginx — onde não tem aplicação nenhuma. Conexão recusada, 502.

No Docker Compose o endereço é o nome do serviço, com a porta interna:

services:
  app:
    build: .
    expose: ["3000"]        # interna, não precisa publicar
  nginx:
    image: nginx
    ports: ["443:443"]      # só o nginx sai para o mundo

# no nginx.conf:  proxy_pass http://app:3000;

O irmão gêmeo do erro está na aplicação: escutar em 127.0.0.1 dentro do container também bloqueia todo mundo de fora. Em Node, é a diferença entre app.listen(3000) e app.listen(3000, '0.0.0.0').

2. A barra final do proxy_pass muda tudo

Não é detalhe estético, é a diferença entre funcionar e dar 404:

  • proxy_pass http://app; — sem nada depois do host — manda o caminho inteiro. /api/users chega como /api/users.
  • proxy_pass http://app/; — com qualquer coisa, nem que seja só a barra — substitui o trecho que casou com o location. Com location /api/, o /api/users chega como /users.

E a combinação que morde: location /api (sem barra) com proxy_pass http://app/ (com barra) entrega //users no destino, com barra dupla. Muita aplicação devolve 404 nisso e a pessoa passa a tarde procurando erro na rota.

3. A aplicação ignora os cabeçalhos que você mandou

Você configurou X-Forwarded-Proto certinho e a aplicação continua gerando link http://. É porque frameworks ignoram esses cabeçalhos por padrão — e fazem certo, porque cabeçalho é texto que qualquer um pode forjar. Você precisa dizer explicitamente que confia no proxy:

// Express
app.set('trust proxy', 1)      // 1 = confia em um proxy à frente

O mesmo vale em Django (USE_X_FORWARDED_HOST, SECURE_PROXY_SSL_HEADER), Rails e Laravel (TrustProxies).

Nunca use trust proxy com true exposto direto na internet. Isso manda confiar em qualquer X-Forwarded-For que chegar, e aí qualquer pessoa forja o próprio IP — o que quebra rate limit por IP e polui o log. Confie no número de saltos que você realmente tem.

4. WebSocket precisa de duas linhas a mais

Sem elas o handshake de upgrade não passa. O sintoma engana: a página abre normalmente, e o socket fica em loop de reconexão.

proxy_http_version 1.1;
proxy_set_header Upgrade    $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;      # socket ocioso não pode cair em 60s

5. O buffer que ajuda no HTTP atrapalha no streaming

Aquele buffer que multiplica sua capacidade tem um efeito colateral: ele segura a resposta. Para HTML e JSON, ótimo. Para Server-Sent Events, streaming de resposta de IA ou log ao vivo, é fatal — o cliente não recebe nada, e depois recebe tudo de uma vez.

Desligue só na rota que faz streaming:

location /eventos {
    proxy_pass http://app:3000;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    chunked_transfer_encoding on;
}

Cloudflare também é um proxy reverso

Muita gente usa um sem saber. Quando você aponta o domínio para o Cloudflare com a nuvenzinha laranja ligada, ele passa a ser um proxy reverso na frente do seu — e ganha os mesmos poderes: TLS, cache, bloqueio.

A consequência prática: o seu nginx passa a ver o IP do Cloudflare, não o do visitante. Todo o trabalho da armadilha 3 vira inútil, porque o X-Real-IP que você preenche já é o IP errado. O conserto é ensinar o nginx a confiar nas faixas do Cloudflare e ler o cabeçalho que ele envia:

# faixas oficiais em https://www.cloudflare.com/ips/
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# ... demais faixas ...
real_ip_header CF-Connecting-IP;

Sem isso, seu rate limit por IP limita o Cloudflare inteiro de uma vez — ou seja, todo mundo junto.


Nginx, Caddy, Traefik: qual escolher

Não existe melhor absoluto. Existe melhor para o seu caso:

Melhor para O preço
Nginx quase tudo; é o padrão da indústria e tem resposta pronta para qualquer problema HTTPS é por sua conta (certbot), e a configuração é manual
Caddy começar rápido: HTTPS automático de verdade, config de cinco linhas menos material quando algo raro dá errado
Traefik Docker e Kubernetes: descobre os serviços sozinho por label, sem editar arquivo conceito próprio para aprender; excesso de mágica quando você quer controle fino
Cloudflare camada de borda: cache global e proteção contra ataque não substitui o seu proxy — complementa

Se você está começando e quer HTTPS funcionando hoje, Caddy é o caminho mais curto. Se a stack é Docker Compose com serviços entrando e saindo, Traefik economiza manutenção. Para todo o resto — e para aprender o que de fato acontece — nginx.


Quando quebra

Proxy reverso quebrado tem um sintoma clássico: 502 Bad Gateway. A página não diz nada, mas o error.log diz tudo, e a linha dele identifica qual das causas é a sua.

Fiz uma ferramenta para isso: cole a linha do log no diagnóstico de Nginx 502 e ela aponta a causa, o que fazer e o comando. Ela também monta o bloco proxy_pass para o seu cenário e mostra exatamente qual caminho vai chegar no destino — a armadilha 2, resolvida visualmente.

E se o destino do seu proxy for um serviço dentro do Kubernetes, o endereço interno tem regra própria: está no montador de DNS interno do Kubernetes e em rota interna vs externa no Kubernetes.


O resumo que vale colar na parede

  • Proxy comum protege o cliente; proxy reverso protege o servidor. Quem instalou é quem ele protege.
  • São duas conexões, não uma — por isso os cabeçalhos X-Forwarded-* existem, e por isso a aplicação precisa ser avisada de que pode confiar neles.
  • Os quatro proxy_set_header não são opcionais.
  • A barra final do proxy_pass muda o caminho que chega no destino.
  • localhost dentro de container aponta para o próprio container.
  • Buffer ajuda no HTTP comum e atrapalha em streaming.
Newsletter

Receba os artigos novos por e-mail

Sem spam. Só o aviso quando sai um tutorial ou artigo novo.