Antes de tudo: 502 não é 504
Os dois aparecem como “o site caiu”, mas apontam para lados opostos, e confundir os dois faz você procurar no lugar errado por horas.
| 502 Bad Gateway | 504 Gateway Timeout | |
|---|---|---|
| O que houve | o nginx falou com a aplicação e a resposta não prestou — ou nem conseguiu conectar | o nginx conectou e a aplicação não respondeu a tempo |
| Causa típica | processo morto, porta errada, host errado, resposta inválida | consulta lenta, laço infinito, worker travado |
| Onde mexer | na aplicação e no endereço do proxy_pass |
na aplicação — e, só depois, em proxy_read_timeout |
As duas armadilhas que respondem pela maioria dos 502
1. localhost dentro de container não é o que você pensa
Esta é, de longe, a campeã. Você põe proxy_pass
http://localhost:3000 no nginx que roda em container, e o
localhost ali é o próprio container do nginx —
onde não tem aplicação nenhuma escutando. Conexão recusada, 502.
No Docker Compose, o endereço certo é o nome do serviço
exatamente como está no YAML, e a porta é a interna do
container, não a que você publicou com ports::
services:
app: # ← este nome é o host
build: .
expose: ["3000"] # interna; não precisa de ports:
nginx:
image: nginx
ports: ["80:80"] # só o nginx precisa publicar
# no nginx.conf: proxy_pass http://app:3000;
O irmão gêmeo desse erro está na aplicação: se ela escuta em
127.0.0.1:3000 dentro do container, ninguém de fora alcança.
Tem que escutar em 0.0.0.0. Em Node é a diferença entre
app.listen(3000) e app.listen(3000, '0.0.0.0').
2. O nginx resolve o nome uma vez só, e guarda para sempre
Esta é traiçoeira porque funciona, e depois para. Quando o
proxy_pass tem um nome literal, o nginx resolve esse nome
ao carregar a configuração e fica com aquele IP. Se o
container da aplicação reinicia e recebe outro IP — o que acontece o tempo
todo — o nginx continua batendo no IP velho. 502 até você recarregar o
nginx.
O sintoma que denuncia: “sobe tudo junto e funciona; derrubo só a app e subo de novo, e quebra”. A correção é forçar o nginx a resolver a cada requisição, usando o DNS interno do Docker e uma variável:
resolver 127.0.0.11 valid=10s; # DNS embutido do Docker
location /api/ {
set $upstream http://app:3000; # variável obriga a resolver de novo
proxy_pass $upstream;
}
proxy_pass usa variável, o nginx não remove mais o
prefixo do location — o caminho vai inteiro. Se você dependia
da barra final para cortar o /api/, precisa fazer isso com
rewrite.
A regra da barra no proxy_pass
Vale decorar, porque não é intuitiva e o simulador lá em cima existe por causa dela:
Sem barra — proxy_pass http://app; — o nginx
manda o caminho inteiro, do jeito que chegou.
Com qualquer coisa depois do host, nem que seja só uma
barra — proxy_pass http://app/; — o nginx
troca o pedaço que casou com o location por
aquilo. Então /api/users com location /api/ vira
/users.
A combinação que morde: location /api (sem barra) com
proxy_pass http://app/ (com barra). O trecho que casa é
/api, ele é trocado por /, e o que chega no
destino é //users — com barra dupla. Muita aplicação devolve
404 nisso.
location com expressão regular, o proxy_pass
não pode ter caminho depois do host. Se puser, o nginx nem
inicia.
Os cabeçalhos que quase todo mundo esquece
Não causam 502, mas causam o problema seguinte: a aplicação gerando link
com o host errado, http:// no lugar de https://,
e todo log com o IP do nginx no lugar do IP do visitante.
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;
Em Express, lembre de app.set('trust proxy', 1) — sem isso o
Node ignora o X-Forwarded-* e você fica com o mesmo problema
mesmo tendo mandado o cabeçalho certo.
WebSocket exige mais duas linhas
Sem elas o handshake de upgrade não passa, e o sintoma é confuso: a página abre, mas o socket cai em loop de reconexão, às vezes com 502.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
O roteiro, quando nada acima serviu
1. A aplicação está de pé e escutando onde você acha?
ss -ltnp | grep 3000 # na máquina
docker compose ps # containers de pé?
2. O nginx alcança a aplicação? Pergunte de dentro do container do nginx, não da sua máquina — é outro ponto de vista:
docker compose exec nginx wget -qO- http://app:3000/ || echo FALHOU
3. A configuração que está rodando é a que você editou? Erro comum: editar o arquivo e esquecer de recarregar, ou editar um arquivo que não é incluído.
nginx -T | grep -A5 proxy_pass # mostra a config REAL, já montada
nginx -t && nginx -s reload # testa e recarrega
4. É SELinux? Em RHEL, Rocky, AlmaLinux e CentOS, o nginx sai da caixa proibido de abrir conexão de rede — e o log diz Permission denied mesmo com tudo certo:
setsebool -P httpd_can_network_connect 1
Se o seu upstream é um Service do Kubernetes e o problema é o endereço, o montador de DNS interno do Kubernetes resolve o outro lado da história.