Configurar o nginx como reverse proxy
De proxy_pass ao upgrade de WebSocket: o guia completo do nginx como reverse proxy, incluindo os quatro cabeçalhos sem os quais a sua aplicação vê todos os visitantes como 127.0.0.1.
Quase todas as aplicações modernas ficam à escuta algures numa porta alta: o Node na 3000, um container Docker na 8080, o Gunicorn na 8000, um servidor de aplicações Java na 8443. Isso não se coloca diretamente na Internet. À frente pertence um reverse proxy e, na prática, esse reverse proxy é o nginx.
A configuração básica cabe em cinco linhas. E é exatamente aí que está o problema: as cinco linhas parecem funcionar e, três semanas depois, repara que todos os visitantes surgem no log da aplicação como 127.0.0.1, que o rate limit do início de sessão bloqueia toda a gente ao mesmo tempo e que o email de reposição da palavra-passe contém uma ligação para http://127.0.0.1:3000. Este artigo trata precisamente desses pontos.
O pré-requisito é ter o nginx instalado. Se ainda não for o caso, ajuda o guia instalar o nginx no Debian e no Ubuntu.
O que um reverse proxy faz tecnicamente
O nginx aceita a ligação do visitante e abre em seguida uma segunda ligação, própria, à aplicação. Este é o ponto decisivo, do qual decorrem todos os restantes problemas.
Do ponto de vista da aplicação, o cliente não é o visitante, é o nginx. O IP de origem é 127.0.0.1. O protocolo é http, mesmo que lá fora estivesse a correr HTTPS. O cabeçalho Host é, por omissão, 127.0.0.1:3000 e não app.example.com. E é HTTP/1.0 em vez de HTTP/1.1, razão pela qual os WebSockets falham sempre sem configuração adicional.
Tudo aquilo que a aplicação deve saber sobre o visitante real tem de lhe ser entregue ativamente pelo nginx sob a forma de cabeçalhos HTTP. Por si só, isso não acontece.
A configuração básica e o sítio onde ela deve ficar
O local de armazenamento difere consoante o sistema, e isso confunde-se com regularidade.
Debian e Ubuntu: a configuração fica em /etc/nginx/sites-available/app.conf e é ativada através de um symlink em /etc/nginx/sites-enabled/. Caso contrário, o bloco de servidor predefinido default intercepta todos os pedidos.
AlmaLinux, Rocky, RHEL e Oracle Linux: aí não existe sequer sites-available. O ficheiro vai diretamente para /etc/nginx/conf.d/app.conf e fica assim imediatamente ativo.
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Ativar no Debian e no Ubuntu:
ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx
Executar nginx -t antes de cada recarregamento não é uma cortesia, é uma obrigação. Um systemctl reload com uma configuração defeituosa mantém o processo antigo a correr, mas um reinício posterior do servidor deixa de conseguir arrancar o nginx.
Todos os comandos desta secção pressupõem privilégios de root; caso contrário, use-os com sudo à frente. Isto vale também para os simples comandos de verificação: nginx -t e nginx -T executados como utilizador normal não falham por causa da configuração, falham por causa de um ficheiro que nem sequer têm permissão para escrever. A mensagem [emerg] open() "/run/nginx.pid" failed (13: Permission denied) seguida de configuration file /etc/nginx/nginx.conf test failed não significa, portanto, que a sua configuração esteja avariada.
Esta configuração encaminha os pedidos. Mesmo assim está avariada, e de uma forma que só se manifesta mais tarde.
Os quatro cabeçalhos sem os quais a aplicação fica cega
Estas quatro linhas pertencem a todos os blocos location com proxy_pass:
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
Host
Sem esta linha, o nginx define Host: 127.0.0.1:3000. Os sintomas: o Django responde com Invalid HTTP_HOST header e devolve 400. O Laravel e o WordPress geram URLs absolutos para 127.0.0.1, visíveis nos redirecionamentos e em cada email enviado. Um backend que serve vários domínios entrega sempre o conteúdo do domínio errado.
A escolha certa aqui é $host e não $http_host: $host contém o nome de host sem porta e recorre ao server_name caso um cliente não envie qualquer cabeçalho Host. Já $http_host deixa passar aquilo que chegar, indicação de porta incluída.
X-Real-IP
$remote_addr é o IP com o qual o nginx fala efetivamente. Exatamente um endereço, sem vírgulas, sem necessidade de parsing. Para aplicações que conhecem apenas um campo para o IP do cliente, este é o caminho mais simples.
X-Forwarded-For
$proxy_add_x_forwarded_for pega num X-Forwarded-For eventualmente já existente e acrescenta-lhe $remote_addr à direita. Com vários proxies forma-se assim uma cadeia.
E é aqui que está uma falha de segurança que quase nenhum guia refere: o próprio cliente pode enviar um X-Forwarded-For. Se o seu nginx estiver diretamente na Internet, o $proxy_add_x_forwarded_for junta numa mesma lista uma indicação livremente inventada pelo atacante e o IP real do cliente. Se a sua aplicação ler então a primeira entrada como IP do cliente, passa a acreditar em qualquer endereço. Rate limits, bloqueios por IP e lógica geográfica ficam assim contornáveis.
Daqui decorrem duas consequências claras:
- A cadeia lê-se sempre da direita para a esquerda. A última entrada é a única que o seu próprio proxy escreveu.
- Se o nginx for a única instância à frente da aplicação, é preferível substituir a cadeia por completo em vez de a acrescentar:
proxy_set_header X-Forwarded-For $remote_addr;
Assim desaparece qualquer histórico falsificado. Só quando existe à frente um load balancer ou um CDN em que confia mesmo é que $proxy_add_x_forwarded_for é a opção certa. Nesse caso deve configurar adicionalmente o módulo realip, para que o próprio nginx conheça o IP verdadeiro e não registe o do elemento anterior:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
A indicação set_real_ip_from é uma whitelist. Sem ela o módulo não tem qualquer efeito e, com uma indicação demasiado ampla como 0.0.0.0/0, torna-se uma porta escancarada.
X-Forwarded-Proto
A aplicação vê uma ligação puramente HTTP, aconteça o que acontecer lá fora. Se este cabeçalho faltar, o que costuma suceder é o seguinte: a aplicação repara que "não há HTTPS" e redireciona para HTTPS, o nginx aceita o pedido, desencripta, volta a encaminhá-lo como HTTP e a aplicação redireciona outra vez. O browser devolve ERR_TOO_MANY_REDIRECTS. Igualmente frequente: os cookies com a flag Secure não são definidos, os inícios de sessão falham sem qualquer mensagem de erro e as páginas carregam imagens e scripts por http://, algo que o browser bloqueia como mixed content.
Use $scheme e não o valor fixo "https". Caso contrário, também o bloco da porta 80 afirma que a ligação foi encriptada.
A prova de que os cabeçalhos chegam mesmo
Em vez de adivinhar, construa um espelho. Este bloco de servidor adicional responde a cada pedido com os cabeçalhos recebidos em texto simples:
server {
listen 127.0.0.1:9999;
default_type text/plain;
location / {
return 200 "Host: $host\nX-Real-IP: $http_x_real_ip\nX-Forwarded-For: $http_x_forwarded_for\nX-Forwarded-Proto: $http_x_forwarded_proto\nProtocolo: $server_protocol\n";
}
}
Aponte o proxy_pass, a título de teste, para http://127.0.0.1:9999, recarregue o nginx e abra a página. O que aí aparecer é exatamente aquilo que a sua aplicação teria recebido. Se as quatro linhas estiverem preenchidas e o IP corresponder à sua ligação real, a configuração está correta. Depois volte a remover o bloco de teste.
A aplicação tem, no entanto, de avaliar também os cabeçalhos. O Express precisa de app.set('trust proxy', 1), o Symfony da definição trusted_proxies, o Django de USE_X_FORWARDED_HOST e SECURE_PROXY_SSL_HEADER. Sem este interruptor, as frameworks ignoram os cabeçalhos de propósito, precisamente pelo motivo de spoofing descrito acima.
proxy_pass e a barra que muda tudo
O erro silencioso mais frequente de toda a configuração do nginx. Um único caráter decide qual o caminho que chega ao backend.
| Configuração | Pedido | O backend recebe |
|---|---|---|
location /api/ { proxy_pass http://127.0.0.1:3000; } | /api/users | /api/users |
location /api/ { proxy_pass http://127.0.0.1:3000/; } | /api/users | /users |
location /api/ { proxy_pass http://127.0.0.1:3000/v2/; } | /api/users | /v2/users |
A regra: assim que existe qualquer caminho a seguir ao host e à porta, nem que seja apenas uma barra, o nginx substitui a parte do URL que corresponde ao prefixo do location. Sem indicação de caminho, o URL completo é encaminhado sem alterações.
O quadro de erro é característico: a página inicial funciona, mas tudo o que está abaixo de um prefixo devolve 404 e, no log do backend, aparecem caminhos com prefixo duplicado como /api/api/users. Um olhar ao log da aplicação esclarece isto em segundos, ao passo que andar a adivinhar no nginx custa horas.
Há dois casos especiais que produzem mensagens de erro próprias. Num location com expressão regular, a indicação de caminho é proibida, caso contrário o nginx aborta no arranque com nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, inside named location, or inside "if" statement. E assim que usar uma variável em proxy_pass, por exemplo proxy_pass http://$backend;, o nginx deixa de resolver o nome no arranque e passa a resolvê-lo em tempo de execução. Sem uma linha resolver no bloco de servidor, isso acaba em no resolver defined to resolve ... e num 502.
Encaminhar WebSockets
O nginx fala com o backend, por omissão, em HTTP/1.0. O HTTP/1.0 não conhece o mecanismo de upgrade. Por isso, qualquer WebSocket falha com a configuração básica, por muito correto que esteja tudo o resto.
Sintomas típicos: a consola do browser indica WebSocket connection to 'wss://app.example.com/ws' failed, muitas vezes com o acrescento Error during WebSocket handshake: Unexpected response code: 400. O Socket.io recua silenciosamente para long polling, a aplicação limita-se a parecer lenta e, no log de acessos, repetem-se sem fim linhas com /socket.io/?EIO=4&transport=polling.
Primeiro o mapeamento, que decide se uma ligação quer sequer um upgrade. Este pertence ao contexto http, ou seja, de preferência a um ficheiro próprio /etc/nginx/conf.d/websocket.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Se este bloco for parar por engano a um bloco server ou location, o nginx deixa de arrancar: nginx: [emerg] "map" directive is not allowed here.
Depois, no bloco location:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Porquê o mapeamento e não simplesmente proxy_set_header Connection "upgrade";? Porque assim também os pedidos HTTP perfeitamente normais passam a pedir um upgrade. Alguns backends respondem a isso com 400 e a reutilização de keepalive deixa de existir. O mapeamento envia upgrade apenas quando o cliente pediu efetivamente um e, nos restantes casos, envia close.
O teste: nas ferramentas de programador do browser, o pedido de WebSocket tem de mostrar o estado 101 Switching Protocols. Tudo o resto, em especial 200 ou 400, significa que o upgrade não passou. Na linha de comandos também funciona sem browser:
curl -sSi -o /dev/null -w '%{http_code}\n' \
-H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
http://127.0.0.1/ws
Definir corretamente os timeouts
Se uma ligação cai de forma reproduzível ao fim de exatamente 60 segundos, isso não é coincidência, é o valor predefinido de proxy_read_timeout. Importante para perceber o mecanismo: o valor não limita a duração total do pedido, limita a pausa entre duas operações de leitura. Um download que demora dez minutos, mas fornece dados continuamente, corre sem problemas até ao fim. Um WebSocket em que nada acontece durante 61 segundos é posto fora.
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
O proxy_connect_timeout vale apenas para o estabelecimento da ligação e está limitado a 75 segundos. Definir um valor mais alto não serve de nada. Num backend local, 10 segundos já é generoso, e um valor baixo permite-lhe perceber mais depressa que o serviço nem sequer está a correr.
Para WebSockets, o melhor caminho não é proxy_read_timeout 86400s;, é um heartbeat na aplicação que envia um ping a cada 30 segundos. Assim o timeout mantém-se como proteção contra ligações penduradas, em vez de ficar praticamente desligado.
Há mais dois valores predefinidos que atacam com regularidade. O client_max_body_size está em 1 MB, e qualquer upload maior acaba em 413 Request Entity Too Large e na linha de log client intended to send too large body. E, com Server-Sent Events ou respostas em streaming, o visitante fica muito tempo sem receber nada, porque o nginx faz buffering. Nesse caso ajuda proxy_buffering off; no bloco location em causa, de forma dirigida e não global.
Colocar HTTPS à frente
O caminho mais cómodo é o certbot com o plugin para nginx. Lê o bloco de servidor existente, acrescenta a parte de TLS e configura a renovação:
apt-get install -y certbot python3-certbot-nginx
certbot --nginx -d app.example.com
Estes nomes de pacote valem para Debian e Ubuntu. No AlmaLinux, no Rocky Linux e no Oracle Linux, o certbot não está nos repositórios base, e um simples dnf install -y certbot python3-certbot-nginx acaba aí com Error: Unable to find a match. O EPEL é obrigatório:
dnf install -y epel-release
dnf install -y certbot python3-certbot-nginx
No Oracle Linux 9, o pacote EPEL chama-se oracle-epel-release-el9 e, se for caso disso, o repositório tem de ser ativado antes com dnf config-manager --enable ol9_developer_EPEL.
Para vários subdomínios vale a pena consultar os certificados wildcard da Let's Encrypt.
Uma diferença de versões que gera avisos quando se copiam configurações alheias: até ao nginx 1.24, o HTTP/2 ativa-se na linha listen; a partir do nginx 1.25.1 existe uma diretiva própria para isso. O Debian 13 traz o nginx 1.26.3 e quer a grafia nova, ao passo que o Debian 12 (1.22.1), o Ubuntu 24.04 (1.24.0) e o Ubuntu 22.04 (1.18.0) querem a antiga. Do lado da Red Hat, a fronteira passa exatamente no mesmo sítio: o AlmaLinux 10 traz a 1.26.3 e, com ela, a forma nova, enquanto o AlmaLinux 9, o Rocky Linux 9 e o Oracle Linux 9 trazem a 1.20.1 e precisam da antiga.
# nginx a partir da 1.25.1, ou seja, entre outros, o Debian 13
listen 443 ssl;
http2 on;
# nginx até à 1.24, ou seja, Debian 12, Ubuntu 24.04 e 22.04
listen 443 ssl http2;
Se usar a forma antiga num nginx recente, o nginx -t indica: nginx: [warn] the "listen ... http2" directive is deprecated, use the "http2" directive instead. É apenas um aviso, ou seja, continua a funcionar. Já a forma nova num nginx antigo é um erro de arranque grave: unknown directive "http2".
Para terminar, o ponto mais importante de todo o exercício: o backend não pode estar ele próprio ligado à Internet. Um reverse proxy não serve de nada se http://server-ip:3000 continuar acessível diretamente, porque nesse caso qualquer pessoa define os cabeçalhos como bem entender. Ligue o serviço a 127.0.0.1.
No Docker, esta é uma armadilha particularmente afiada: -p 3000:3000 publica a porta em todos os endereços e cria para isso regras que simplesmente contornam uma firewall ufw. O correto é -p 127.0.0.1:3000:3000. Os detalhes da instalação estão em instalar o Docker no Debian e no Ubuntu.
Se, excecionalmente, fizer proxy para um backend HTTPS, o nginx precisa de uma linha adicional; caso contrário não envia qualquer nome SNI e o outro lado entrega o certificado errado:
proxy_pass https://backend.intern:8443;
proxy_ssl_server_name on;
Quando corre mal: as mensagens na íntegra
O primeiro sítio a consultar é sempre tail -f /var/log/nginx/error.log. A página de erro do browser não diz nada, o log diz tudo.
Uma pequena nota prévia, para que já a primeira linha não confunda: no AlmaLinux, no Rocky Linux e no Oracle Linux, o ficheiro /var/log/nginx/error.log ainda não existe logo a seguir à instalação, só nasce no primeiro arranque do nginx. O tail responde então com cannot open ... No such file or directory. No Debian e no Ubuntu, o pacote cria access.log e error.log logo na instalação. Uma forma robusta que funciona nos dois mundos:
tail -n 50 /var/log/nginx/error.log 2>/dev/null || journalctl -u nginx -n 50 --no-pager
| Mensagem no error.log | Significado e solução |
|---|---|
connect() failed (111: Connection refused) while connecting to upstream | Nada está à escuta na porta de destino. Verifique o estado do serviço e confirme com ss -ltnp se a porta e o endereço correspondem à linha proxy_pass. Se faltar o ss, ele vem no Debian e no Ubuntu com o pacote iproute2 e, na família Red Hat, com o iproute (aí sem o 2 no nome). |
connect() failed (113: No route to host) | Uma firewall entre o nginx e o backend está a bloquear. Com containers, é muitas vezes uma rede errada. |
connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstream | No AlmaLinux, Rocky, RHEL e Oracle Linux é quase sempre o SELinux. Comprova-se com ausearch -m AVC -ts recent e resolve-se com setsebool -P httpd_can_network_connect 1. No Debian e no Ubuntu isto não acontece. |
upstream timed out (110: Connection timed out) while reading response header from upstream | Dá origem a um 504. O backend responde demasiado devagar. Vá primeiro ver lá e só depois aumente o proxy_read_timeout. |
upstream prematurely closed connection while reading response header | Dá origem a um 502. O processo do backend morreu durante o pedido, muitas vezes às mãos do OOM killer. Ver configurar swap. |
upstream sent too big header while reading response header from upstream | Cabeçalhos de resposta demasiado grandes, um clássico com cookies numerosos ou longos. Defina proxy_buffer_size 32k; e proxy_buffers 8 32k;. |
no live upstreams while connecting to upstream | Num bloco upstream, todos os destinos foram marcados como indisponíveis. Controle o comportamento de verificação de saúde através de max_fails e fail_timeout. |
Para os casos especiais em torno do 502 existe um guia próprio: resolver o nginx 502 Bad Gateway. Se o seu backend ainda não for um serviço que arranque de forma limpa, vale a pena antes criar um serviço systemd.
Como reconhece que está mesmo a funcionar
Cinco verificações que, em conjunto, são conclusivas:
- O
nginx -tindicasyntax is oketest is successful. - O
nginx -Tmostra a configuração completa já montada. Procure aí porproxy_set_headere conte: os quatro cabeçalhos têm de estar em cada bloco location relevante. Umproxy_set_headernum bloco interior anula todos os cabeçalhos herdados do bloco exterior, e essa é a causa mais frequente do clássico "mas eu tinha isto definido". - No log da aplicação aparece o IP real do visitante e não
127.0.0.1. - O bloco de servidor espelho apresentado acima devolve os quatro valores preenchidos, com
X-Forwarded-Proto: httpsno acesso por HTTPS. - Para WebSockets: estado 101 nas ferramentas de programador, e a ligação sobrevive a mais de 60 segundos de silêncio.
Ajuda ainda um formato de log que registe também o IP encaminhado. Assim vê de imediato se o nginx e a aplicação se referem ao mesmo endereço:
log_format proxied '$remote_addr xff="$http_x_forwarded_for" '
'host=$host "$request" $status $body_bytes_sent '
'upstream=$upstream_addr rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/app.access.log proxied;
Os dois valores de tempo no fim valem ouro: $request_time é a duração total do ponto de vista do visitante e $upstream_response_time é a duração da resposta do backend. Se ambos estiverem próximos um do outro, o backend é lento. Se houver uma diferença grande, a culpa é da ligação até ao cliente ou do buffering.
Fica assim montado um reverse proxy que não se limita a tornar a aplicação acessível, mas lhe entrega também tudo aquilo que ela precisa de saber sobre os seus visitantes. Se montar esta configuração num sistema acabado de instalar, a checklist para novos servidores root é um bom ponto de partida para tudo o que vem antes.
Perguntas frequentes
Porque é que a minha aplicação vê 127.0.0.1 em vez do IP real do visitante?
Qual é a diferença entre X-Real-IP e X-Forwarded-For?
Porque é que os meus WebSockets não funcionam por trás do nginx?
Porque é que a minha ligação cai sempre ao fim de exatamente 60 segundos?
O que provoca a barra no fim de proxy_pass?
Porque é que recebo um 502 com Permission denied no AlmaLinux?
2026 KernelHost GmbH. Todos os direitos reservados. Este guia está protegido por direitos de autor. A sua republicação noutros sites, na íntegra, em parte ou de forma editada, não é permitida sem o nosso consentimento por escrito. Citações com indicação da fonte e ligação são expressamente bem-vindas.

