Nginx 502 Bad Gateway

A página de erro não diz nada. O error.log diz tudo. Cole a linha dele aqui e descubra qual das treze causas é a sua.

Roda no seu navegador · nada é enviado para servidor

Cole a linha do error.log

Onde achar: tail -50 /var/log/nginx/error.log — ou docker logs <container-do-nginx>. Pegue a linha do horário em que o 502 aconteceu.

Montar o proxy

Onde a aplicação roda

Para onde a URL vai

A barra final no proxy_pass muda o caminho que chega no destino. É a confusão nº 1 do nginx, e a causa de metade dos 404 que aparecem depois de resolver o 502.

proxy_passchega no destino

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 Gateway504 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
Aumentar timeout para resolver 502 não funciona: se a conexão foi recusada, esperar mais tempo pela recusa não muda nada. Timeout só trata 504.

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;
}
Cuidado com o efeito colateral: quando o 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 barraproxy_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.

Em 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.

Newsletter

Receba os artigos novos por e-mail

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