← Voltar para o Blog

nginx como reverse proxy: os erros que só aparecem em produção

A configuração funciona no teste e quebra com tráfego real: upload que falha, WebSocket que cai, IP do cliente que vira 127.0.0.1. São sempre as mesmas oito diretivas.

Equipe EasyOps Cloud · · 7 min de leitura

Um bloco location com proxy_pass tem três linhas e coloca a aplicação no ar. Foi assim que quase toda configuração de nginx em produção nasceu — e é por isso que quase toda configuração de nginx em produção tem os mesmos problemas.

Eles não aparecem no teste. Aparecem quando chega upload de verdade, cliente com conexão ruim, WebSocket e alguém precisando saber de qual IP veio uma requisição. São oito situações, e cada uma tem uma diretiva específica.

502 e 504 não são o mesmo problema

O erro mais reportado, e o mais mal diagnosticado. Os dois códigos apontam para lugares opostos:

CódigoSignificaOnde investigar
502 Bad GatewayO nginx não conseguiu falar com a aplicaçãoA aplicação caiu, ou a porta está errada
504 Gateway TimeoutFalou, e a aplicação não respondeu a tempoA aplicação está lenta, não morta

O log do nginx diz qual dos dois com precisão:

sudo tail -f /var/log/nginx/error.log

connect() failed (111: Connection refused) é 502: não há nada escutando. Confirme com ss -tulpn | grep 3000 e olhe o serviço.

upstream timed out (110: Connection timed out) while reading response header é 504: a aplicação recebeu e demorou. Aumentar o timeout do nginx aqui é maquiagem — ele passa a esperar mais por uma resposta que continua lenta. Ainda assim, há casos legítimos, como relatório pesado ou exportação:

location /relatorios/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

Aplique no location específico, nunca no server inteiro. Timeout global de 300s transforma uma aplicação travada em mil conexões presas.

O IP do cliente vira 127.0.0.1

Sem os cabeçalhos de encaminhamento, a aplicação enxerga todas as requisições como vindas do próprio servidor. Isso quebra log de auditoria, geolocalização e — o pior — *rate limiting*, que passa a contar todo mundo como um único cliente.

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;

O X-Forwarded-Proto merece destaque. Sem ele, a aplicação recebe uma conexão HTTP comum e não sabe que o cliente falou HTTPS. O sintoma clássico é o formulário que gera link http:// numa página servida em https://, e o navegador bloqueia por conteúdo misto.

Vale saber que esses cabeçalhos são texto: qualquer cliente pode enviar um X-Forwarded-For falso. A aplicação só deve confiar neles se souber que a conexão veio do proxy — o que significa a aplicação escutando em 127.0.0.1 e o firewall fechando a porta dela para o mundo.

A barra do proxy_pass muda tudo

A pegadinha mais silenciosa de todas. Uma barra no fim da URL do proxy_pass altera o caminho enviado ao backend:

# SEM barra: envia /api/usuarios
location /api/ {
    proxy_pass http://127.0.0.1:3000;
}

# COM barra: envia /usuarios — o /api/ é removido
location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}

Nenhuma das duas está errada; erradas são as expectativas trocadas. O sintoma é 404 vindo da aplicação para uma rota que existe — ou uma rota que responde no lugar de outra. Ao depurar 404 atrás de proxy, esta é a primeira linha a conferir.

Upload que falha em 1 MB

O padrão do nginx para corpo de requisição é 1 MB. Acima disso, 413 Request Entity Too Large, e a aplicação nem fica sabendo:

client_max_body_size 50m;

Defina no server ou no location de upload, com o mesmo valor configurado na aplicação — os dois limites precisam concordar, senão você troca um erro claro do nginx por um erro obscuro do framework.

Para arquivo grande, vale desligar o buffer intermediário. Por padrão o nginx grava o corpo inteiro em disco antes de repassar, o que dobra a escrita e adiciona latência:

location /upload {
    proxy_pass http://127.0.0.1:3000;
    proxy_request_buffering off;
    client_max_body_size 2g;
}

Se o destino final do arquivo é um bucket, considere não passá-lo pelo servidor: uma URL pré-assinada faz o navegador enviar direto ao bucket S3, e o upload deixa de consumir banda, disco e memória da VM.

WebSocket que conecta e cai

WebSocket começa como HTTP e pede troca de protocolo. Sem repassar esse pedido, o nginx responde 400 ou derruba a conexão após um minuto:

location /ws {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}

As três primeiras linhas fazem a conexão funcionar. A quarta faz ela durar: o proxy_read_timeout padrão de 60s derruba qualquer WebSocket ocioso, e o sintoma é aquele "reconectando" a cada minuto que ninguém consegue explicar.

HTTP/1.0 contra o upstream

Por padrão o nginx fala HTTP/1.0 com o backend e abre uma conexão TCP nova a cada requisição. Sob carga, isso desperdiça CPU e esgota portas efêmeras:

upstream app {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

A linha proxy_set_header Connection "" é obrigatória e frequentemente esquecida — sem ela, o cabeçalho Connection: close é repassado e o keepalive não tem efeito nenhum.

Buffering e a resposta que trava

O nginx acumula a resposta antes de enviar ao cliente. Isso é bom para conexão lenta e péssimo para *streaming*: eventos SSE, saída de log ao vivo e resposta token a token de uma API de IA ficam presos no buffer.

location /stream {
    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
}

Só nesse location. Desligar buffering em tudo faz o servidor pagar o preço de cada cliente lento.

Estático passando pela aplicação

Um erro de arquitetura que se disfarça de erro de configuração: todo o tráfego cai no location /, e o nginx repassa até imagem e CSS para a aplicação. O Node ou o PHP então lê o arquivo do disco e devolve — trabalho que o nginx faz muito melhor e sem ocupar um worker da aplicação.

location /static/ {
    alias /var/www/meuapp/static/;
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
}

location / {
    proxy_pass http://app;
}

O nginx escolhe o location mais específico, então o bloco de estáticos vence sem precisar de ordem especial. O immutable só é seguro quando os arquivos têm hash no nome — se app.css muda de conteúdo mantendo o nome, o navegador vai segurar a versão velha por trinta dias.

Upstream que não sabe quando desistir

Com um único backend, proxy_pass aponta e pronto. Com dois ou mais, o nginx precisa saber quando parar de mandar tráfego para o que está com problema:

upstream app {
    server 10.0.0.11:3000 max_fails=3 fail_timeout=15s;
    server 10.0.0.12:3000 max_fails=3 fail_timeout=15s;
    keepalive 32;
}

Após 3 falhas em 15 segundos, aquele servidor sai do rodízio por 15 segundos. Sem esses parâmetros, o nginx continua enviando um terço das requisições para uma máquina morta.

Vale conhecer o comportamento padrão em caso de erro: o nginx reenvia a requisição para o próximo servidor. Para GET isso é ótimo; para POST pode significar cobrança duplicada. Restrinja quando a operação não for idempotente:

proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;

Erros que aparecem depois

  • Recarregar sem testar. nginx -t antes de todo reload. Erro de sintaxe faz o reload falhar e o processo velho continua rodando com a configuração antiga — você acha que aplicou e não aplicou.
  • `server_name` ausente ou duplicado. O nginx usa o primeiro server como padrão para host desconhecido. Dois blocos com o mesmo nome fazem um deles ser ignorado em silêncio.
  • `gzip` sobre resposta já comprimida. Comprimir JPEG ou vídeo gasta CPU sem reduzir nada. Restrinja por gzip_types.
  • `access_log` sem rotação. Em site movimentado, enche disco em semanas.
  • `resolver` faltando com upstream por nome. Se o proxy_pass aponta para um domínio, o nginx resolve o DNS uma vez, na inicialização. O IP muda, o nginx não percebe, e você tem 502 até reiniciar.

Um bloco de referência

upstream app {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on;
    server_name meusite.com.br;

    client_max_body_size 50m;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;
    }
}
sudo nginx -t && sudo systemctl reload nginx

O proxy_connect_timeout curto é proposital: se a aplicação não aceita a conexão em 5 segundos, ela não vai aceitar em 60 — falhar rápido devolve erro claro em vez de segurar o cliente.

Para o certificado e o apontamento do domínio, o guia de domínio e SSL cobre a parte de DNS. E se depois de tudo o nginx ainda devolver 502, o problema não está aqui — está do outro lado, e o diagnóstico do systemd resolve mais rápido que qualquer ajuste de proxy.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta