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ódigo | Significa | Onde investigar |
|---|---|---|
| 502 Bad Gateway | O nginx não conseguiu falar com a aplicação | A aplicação caiu, ou a porta está errada |
| 504 Gateway Timeout | Falou, e a aplicação não respondeu a tempo | A 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.logconnect() 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 -tantes de todoreload. Erro de sintaxe faz oreloadfalhar 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
nginxusa o primeiroservercomo 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_passaponta para um domínio, onginxresolve o DNS uma vez, na inicialização. O IP muda, onginxnã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 nginxO 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.