Configuração de reverse proxy nginx explicada
Aprenda a montar um server block nginx no Ubuntu 24.04, com proxy_pass, quatro headers, WebSocket, barras finais e uploads sem surpresas.
O que uma configuração de reverse proxy nginx faz
Um reverse proxy nginx recebe os pedidos que chegam às portas 80 e 443 e encaminha cada um para uma aplicação que já está a escutar numa porta local. Depois, devolve ao navegador a resposta dessa aplicação. A configuração é um único bloco server, e esse bloco é curto. Quase toda a dificuldade está em cinco ou seis linhas que informam a aplicação sobre a identidade real do cliente e o protocolo usado por esse cliente.
Tudo o que se segue é configurado de raiz no Ubuntu 24.04, usando o pacote nginx da distribuição. O ponto de partida é uma aplicação que já responde em 127.0.0.1:3000. Se ainda não escolheu um proxy, leia primeiro como o nginx se compara com o Caddy e o Traefik. O que se segue mostra a configuração nginx, linha a linha.
Execute estas configurações no seu próprio servidor. Teste cada alteração com sudo nginx -t antes de recarregar o serviço e leia o resultado apresentado.
Onde o nginx mantém a configuração no Ubuntu
sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/O ficheiro principal é /etc/nginx/nginx.conf. Define opções globais dentro de um bloco http { } e inclui depois dois diretórios: /etc/nginx/conf.d/*.conf e /etc/nginx/sites-enabled/*. No Ubuntu e no Debian, crie um ficheiro por site em /etc/nginx/sites-available/ e ative-o com um link simbólico para /etc/nginx/sites-enabled/. Apagar o link simbólico desativa o site, mas mantém o ficheiro.
Duas diretivas usadas mais tarde só funcionam no contexto http, nunca dentro de um bloco server: map e upstream. Coloque-as no seu próprio ficheiro em /etc/nginx/conf.d/, porque esse diretório é incluído ao nível http.
O pacote instala um site ativo chamado default. Está marcado como default_server, o que significa que responde a qualquer pedido cujo cabeçalho Host não corresponda a nenhum server_name na sua configuração. Enquanto permanecer ativo, um pedido que não corresponda aos seus nomes é encaminhado para esse site, e não para a sua aplicação. Remova o link simbólico quando o seu próprio site estiver a funcionar.
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxO bloco de servidor mais simples que faz proxy de uma aplicação
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}Guarde isto como /etc/nginx/sites-available/app.example.com, depois ative-o e carregue a configuração.
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/listen 80; associa-se a IPv4 e listen [::]:80; associa-se a IPv6. Se deixar a segunda linha de fora e a consulta DNS (domain name system) do visitante devolver um registo AAAA para o seu servidor, a ligação será recusada, enquanto todos os utilizadores em IPv4 verão um site funcional. O relatório de erro que recebe diz: "funciona para mim".
server_name é comparado com o cabeçalho Host enviado pelo navegador. Pode listar vários nomes, separados por espaços. Se nenhum bloco corresponder, o nginx usa o bloco que estiver default_server, motivo pelo qual o site incluído no pacote teve de ser removido.
location / é uma correspondência por prefixo no caminho do pedido, e / corresponde a qualquer caminho. proxy_pass é o endereço ao qual o nginx abre uma ligação. Mantenha a aplicação associada a 127.0.0.1 para que a única entrada seja através do nginx. Se a aplicação for executada num contentor, publique-a como 127.0.0.1:3000:3000 e não como 3000:3000, porque o Docker escreve as suas próprias regras e publica as portas diretamente, ultrapassando o ufw, pelo que uma porta publicada sem restrições fica acessível a partir da Internet, independentemente da configuração da firewall.
A linha curl envia o cabeçalho Host correto a partir do próprio servidor, permitindo testar o bloco antes de o DNS apontar para algum destino.
O que o nginx envia para o upstream quando não configura mais nada
proxy_pass, isoladamente, oculta quatro informações da sua aplicação.
Por padrão, o nginx fala HTTP/1.0 com o backend e envia Connection: close. Por isso, cada pedido abre uma nova ligação ao upstream, e não é possível fazer uma atualização de protocolo.
O cabeçalho Host é reescrito com o valor de proxy_pass, que é 127.0.0.1:3000. Uma aplicação que cria links absolutos a partir de Host passa a produzir links que ninguém fora do servidor consegue abrir.
A ligação que chega à aplicação vem do nginx, por isso a aplicação vê o endereço do cliente como 127.0.0.1. Cada linha de log e cada limite de pedidos dentro da aplicação registam o proxy em vez do visitante.
A aplicação não consegue saber que o browser usou HTTPS, porque a ligação recebida é HTTP simples num endereço de loopback.
Quatro linhas corrigem tudo isso.
Os quatro cabeçalhos a definir e o que cada um permite que o backend veja
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 transporta o nome introduzido pelo visitante. $host é o nome do pedido, sem a porta e com as letras convertidas para minúsculas. Defina-o para que a aplicação construa URLs absolutos corretos, como o redirecionamento depois de um início de sessão ou a ligação numa mensagem de email para reposição da palavra-passe. Se o omitir, esses URLs apontam para 127.0.0.1:3000, e o início de sessão envia o navegador para um endereço que recusa a ligação. Se a aplicação também precisar da porta, porque é servida na porta 8080, use $http_host, que corresponde exatamente ao cabeçalho enviado pelo cliente.
X-Real-IP transporta um valor: $remote_addr, o endereço a partir do qual o nginx aceitou a ligação. As aplicações leem-no nos seus próprios logs de acesso e para aplicar os seus próprios limites de pedidos.
X-Forwarded-For transporta uma lista. $proxy_add_x_forwarded_for acrescenta $remote_addr ao valor que o cliente já colocou nesse cabeçalho, pelo que o resultado é separado por vírgulas e a entrada adicionada pelo nginx é a última. Esse detalhe determina se o cabeçalho pode ser considerado fiável: um cliente pode enviar qualquer X-Forwarded-For que queira, por isso uma aplicação que leia a primeira entrada pode receber qualquer endereço. Quando o nginx é o servidor de edge, escreva $remote_addr em vez disso e descarte a versão do cliente. Quando existe uma CDN ou outro proxy à frente, use set_real_ip_from e real_ip_header do módulo realip, para que $remote_addr passe a ser o endereço real do cliente.
X-Forwarded-Proto transporta http ou https. Os frameworks leem-no para decidir se devem marcar os cookies como Secure e se devem forçar um redirecionamento para HTTPS. Se o omitir num site TLS e a aplicação estiver configurada para forçar HTTPS, ela vê http, responde com um redirecionamento para o endereço HTTPS, recebe o pedido seguinte através do nginx, continua a ver http e redireciona novamente. O navegador desiste e mostra ERR_TOO_MANY_REDIRECTS.
Repetir estas quatro linhas em cada location faz com que se afastem umas das outras. Coloque-as num único ficheiro e inclua-o.
# /etc/nginx/snippets/proxy-headers.conf
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;location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
}A herança tem aqui uma armadilha. Uma location herda as diretivas proxy_set_header do seu bloco server apenas enquanto não define nenhuma diretiva própria. Ao adicionar uma proxy_set_header dentro da location, todos os cabeçalhos definidos ao nível do server são removidos nessa location. Por isso, mantenha todos ao mesmo nível ou include o snippet em cada location que encaminha pedidos por proxy.
Por que a minha aplicação WebSocket se liga e depois desliga?
Isto acontece porque as predefinições não permitem o upgrade, e o tempo limite de leitura predefinido fecha um túnel inativo depois de 60 segundos. Um WebSocket começa como um pedido HTTP que contém Upgrade: websocket e Connection: Upgrade. Estes são cabeçalhos hop-by-hop, o que significa que o proxy deve consumi-los em vez de os encaminhar, e o HTTP/1.0 não tem qualquer mecanismo de upgrade. É necessário repor ambos manualmente.
O map fica no contexto http, no seu próprio ficheiro.
# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Depois, a location.
location / {
include snippets/proxy-headers.conf;
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_read_timeout 3600s;
proxy_send_timeout 3600s;
}O map existe para que uma única location possa servir os dois tipos de tráfego. Num pedido normal, $http_upgrade fica vazio, por isso $connection_upgrade passa a ser close. Num pedido de upgrade, contém websocket, por isso o cabeçalho enviado para o upstream é Connection: upgrade. Definir proxy_set_header Connection "upgrade"; diretamente envia esse cabeçalho também em todos os pedidos de páginas normais, e alguns backends respondem a esse tipo de pedido com 400.
proxy_read_timeout é a causa dos relatos do tipo "carrega e depois deixa de atualizar". O valor predefinido é 60 segundos, e mede o intervalo entre duas leituras do backend, não a duração da ligação. Um WebSocket que permanece inativo durante 60 segundos é fechado pelo nginx, e a consola do browser mostra o fecho do socket com o código 1006. As aplicações que enviam o seu próprio heartbeat mais do que uma vez por minuto não detetam o problema. As aplicações que não o fazem terminam ao fim de um minuto. Editores em tempo real e dashboards são os primeiros locais onde isto aparece, sendo uma instância n8n autoalojada atrás de HTTPS um exemplo comum.
Por que uma barra final em proxy_pass altera as URLs?
A regra cabe numa frase. Se proxy_pass terminar com uma URI (identificador uniforme de recursos), mesmo que seja apenas /, o nginx remove a parte do caminho do pedido que correspondeu ao prefixo location e coloca essa URI no lugar. Se proxy_pass terminar no host e na porta, o caminho do pedido é encaminhado sem alterações.
location /app/ {
proxy_pass http://127.0.0.1:3000/;
}Um pedido para /app/status chega ao backend como /status.
location /app/ {
proxy_pass http://127.0.0.1:3000;
}Um pedido para /app/status chega ao backend como /app/status.
A forma adequada depende da aplicação. Uma aplicação com uma configuração de caminho base ou subpasta precisa da segunda forma, com essa configuração definida para /app. Uma aplicação que não conhece prefixos precisa da primeira. A primeira forma tem um efeito imediato: o HTML devolvido pela aplicação continua a conter caminhos absolutos, como /static/main.css, o navegador pede-os na raiz do site, nenhuma location corresponde e a página é apresentada sem estilos. O separador de rede do navegador mostra esses pedidos de recursos a devolverem 404. A correção é usar a configuração de caminho base da própria aplicação ou adicionar uma segunda location /static/ a apontar para o mesmo backend.
Uma location com expressão regular não pode conter uma URI em proxy_pass. sudo nginx -t recusa a configuração e indica o motivo: "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block.
Esta classe de problemas desaparece quando cada aplicação recebe o seu próprio nome, app.example.com, encaminhado por proxy a partir de location /. Os subcaminhos só compensam quando não é possível adicionar registos DNS.
Como coloco mais de um backend atrás do mesmo nome?
Com um bloco upstream. Ele pertence ao contexto http, por isso deve ser escrito acima do bloco server no mesmo ficheiro ou em /etc/nginx/conf.d/.
upstream app_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
keepalive 32;
}A localização passa a referenciá-lo: proxy_pass http://app_backend;.
O método predefinido é round robin. least_conn envia cada pedido para o backend com menos ligações ativas, o que é adequado para pedidos com duração desigual. ip_hash associa o endereço de cada cliente a um único backend. Precisa de ip_hash quando a aplicação mantém as sessões na própria memória, porque o round robin entre dois backends desse tipo termina as sessões dos utilizadores de forma aleatória quando os pedidos chegam à instância que nunca os recebeu. A melhor solução é mover as sessões para um armazenamento partilhado.
max_fails=3 fail_timeout=30s significa que três tentativas falhadas em 30 segundos retiram esse servidor do conjunto durante 30 segundos. Quando todos os servidores do bloco estão nesse estado, os clientes recebem 502 e o log de erros indica no live upstreams while connecting to upstream.
keepalive 32 mantém abertas até 32 ligações inativas aos backends por processo de worker, eliminando o handshake TCP da maioria dos pedidos. Funciona apenas com proxy_http_version 1.1 e sem Connection: close a encaminhar pedidos para o upstream. Se a mesma localização também utilizar o mapa WebSocket, altere o caso vazio de close para uma string vazia, para que os pedidos normais não enviem o cabeçalho Connection e a ligação agrupada seja reutilizada.
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}Os nomes dentro de um bloco upstream são resolvidos quando o nginx arranca. Se o backend for um contentor que recebe um novo endereço quando reinicia, o nginx continua a utilizar o endereço antigo até ser recarregado. Numa rede Docker, pode mover a resolução para o momento do pedido utilizando o resolver incorporado.
resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;Quando os contentores aparecem e desaparecem com frequência suficiente para ter de editar o nginx continuamente, um proxy que leia labels dos contentores é uma opção melhor. Traefik à frente de várias aplicações Docker Compose cria as rotas a partir dos próprios contentores.
Por que os uploads falham com 413 Request Entity Too Large?
client_max_body_size tem como valor padrão 1 megabyte. Um corpo de pedido maior é recusado pelo nginx antes de a aplicação receber qualquer parte dele, e o log de erros regista client intended to send too large body. Aumente esse valor no bloco server ou no location usado pelos uploads.
client_max_body_size 512m;Um valor de 0 desativa completamente a verificação. A aplicação também tem o seu próprio limite. Por isso, se o erro 413 continuar depois desta alteração, a resposta vem do backend. Verifique a configuração de uploads da própria aplicação.
Por padrão, o nginx lê todo o corpo do pedido antes de abrir a ligação com o upstream. Primeiro, grava os dados grandes num ficheiro temporário no disco. Isto protege a aplicação contra clientes lentos, porque o backend recebe o upload à velocidade da rede local. Para uploads muito grandes, pode fazer streaming em vez disso.
proxy_request_buffering off;Nesse caso, o backend recebe o corpo à medida que ele chega e tem de conseguir processá-lo dessa forma. O nginx também deixa de poder repetir o pedido noutro upstream, porque o corpo já não está disponível.
client_body_timeout, que tem como valor padrão 60 segundos, aplica-se ao intervalo entre duas leituras consecutivas do corpo, e não ao upload completo. Um upload lento mas contínuo não ultrapassa esse limite. Um upload que fica parado é interrompido.
Armazenamento temporário das respostas e a configuração que interrompe a saída em tempo real
proxy_buffering está ativado por padrão e, normalmente, é o comportamento pretendido. O nginx lê a resposta da aplicação à mesma velocidade a que a aplicação a consegue escrever, armazena-a e envia-a para um cliente lento ao ritmo desse cliente. O processo de trabalho da aplicação termina mais cedo, em vez de permanecer ocupado durante toda a transferência lenta.
Isto interrompe as respostas em fluxo contínuo. Os eventos enviados pelo servidor e a saída de logs em tempo real não mostram dados ao leitor até um buffer ficar cheio. Desative o armazenamento temporário apenas nessa localização.
proxy_buffering off;Se controlar a aplicação, a melhor opção é enviar o cabeçalho X-Accel-Buffering: no apenas nas respostas em fluxo contínuo. O nginx lê esse cabeçalho em cada resposta e desativa o armazenamento temporário apenas para ela. Assim, as páginas comuns mantêm essa vantagem.
Quando o log de erros indica upstream sent too big header while reading response header from upstream, os cabeçalhos da resposta não couberam num único buffer. proxy_buffer_size usa por padrão uma única página de memória, com 4 ou 8 kilobytes, dependendo da plataforma. Cookies longos ou cabeçalhos de autenticação grandes excedem esse limite. Aumente os dois valores.
proxy_buffer_size 16k;
proxy_buffers 8 16k;Onde o TLS deve ser configurado nesta configuração?
No Nginx, à frente de tudo o que foi apresentado acima. O TLS (segurança da camada de transporte) termina no proxy, e a ligação do Nginx à aplicação continua como HTTP simples através do endereço de loopback, onde nenhum outro dispositivo da rede a pode ler. A aplicação fica a saber que o visitante usou HTTPS através de X-Forwarded-Proto, o quarto dos quatro cabeçalhos.
Não escreva manualmente os caminhos dos certificados. Aponte o registo DNS para o servidor, abra a firewall e deixe o Certbot editar este mesmo bloco de servidor: ele adiciona a linha listen 443 ssl com os caminhos ssl_certificate e também um redirecionamento da porta 80. Emitir um certificado Let's Encrypt para o Nginx com o Certbot explica a emissão e o temporizador de renovação.
sudo ufw allow 'Nginx Full'
sudo ufw statusNginx Full é um perfil de aplicação instalado pelo pacote do Nginx e abre as portas 80 e 443 em conjunto. A porta 80 tem de continuar aberta para o desafio de renovação HTTP-01, mesmo depois de todos os visitantes serem redirecionados para HTTPS.
Teste a configuração e depois recarregue
sudo nginx -t
sudo systemctl reload nginxnginx -t analisa todos os ficheiros incluídos e indica se o teste foi bem-sucedido ou apresenta o ficheiro e a linha onde parou. Leia essa saída antes de recarregar. Um recarregamento com uma configuração inválida não é aplicado: o nginx continua a servir a configuração anterior, por isso o site permanece disponível enquanto a alteração não produz qualquer efeito. systemctl restart comporta-se de forma diferente e pior, porque um reinício encerra primeiro o servidor em execução. Assim, um erro de configuração deixa o nginx completamente parado. Por predefinição, recarregue a configuração e reserve o reinício para as alterações que realmente o exigem.
sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'A linha ss mostra qual processo mantém cada porta aberta. Assim, pode confirmar que a aplicação está realmente a escutar no local indicado por proxy_pass.
As falhas que você encontrará na prática
502 Bad Gateway, com connect() failed (111: Connection refused) while connecting to upstream no log de erros. Não há nenhum processo a escutar no endereço indicado em proxy_pass. A aplicação está parada, está associada a outra porta ou está associada a um endereço interno do contentor que o host não consegue alcançar.
502 com no live upstreams while connecting to upstream. Todos os servidores do bloco upstream estão atualmente marcados como falhados por max_fails. Corrija os backends. O nginx tenta novamente quando fail_timeout expira.
504 Gateway Time-out, com upstream timed out (110: Connection timed out) while reading response header from upstream. O backend aceitou a ligação e depois não enviou nada durante proxy_read_timeout segundos. Aumentar o timeout é correto para um relatório genuinamente lento, mas está errado quando a aplicação está bloqueada.
Todos os caminhos devolvem 404 pela aplicação. A regra da barra final reescreveu o caminho. Compare o caminho registado pela aplicação com o caminho solicitado.
É apresentado um site diferente. server_name não corresponde ao cabeçalho Host, por isso o pedido passou para o bloco default_server.
A página carrega, mas a interface bloqueia após cerca de um minuto. Este é o caso de WebSocket: falta a configuração de Upgrade ou proxy_read_timeout continua definido como 60 segundos.
FAQ
Por que o nginx retorna 502 Bad Gateway depois de eu adicionar proxy_pass?
O nginx não conseguiu abrir uma ligação para o endereço em proxy_pass. O log de erros em /var/log/nginx/error.log indica a causa: connect() failed (111: Connection refused) while connecting to upstream significa que não há nada a escutar nesse endereço, e no live upstreams significa que todos os servidores num bloco upstream foram marcados como falhados. Execute sudo ss -lntp | grep 3000 para ver que processo ocupa a porta e a que endereço está associado. Uma aplicação associada a um endereço interno do contentor, ou a uma porta diferente da indicada, produz sempre este erro.
Por que a minha aplicação se desliga após cerca de um minuto atrás do nginx?
A ligação é um WebSocket e proxy_read_timeout ainda tem o valor predefinido de 60 segundos, que mede o intervalo entre duas leituras do backend. Um socket inativo é fechado pelo nginx e a consola do navegador apresenta o código de fecho 1006. Defina proxy_http_version 1.1, encaminhe Upgrade e Connection com um map em $http_upgrade e aumente proxy_read_timeout para um valor como 3600s. Sem o cabeçalho Upgrade, a atualização nunca ocorre, pelo que a aplicação recorre ao polling ou não apresenta atualizações em tempo real.
A barra final em proxy_pass é importante?
Sim, e altera o caminho que o backend recebe. Com location /app/ e proxy_pass http://127.0.0.1:3000/, um pedido para /app/status chega ao backend como /status, porque qualquer URI depois do host e da porta substitui o prefixo da localização correspondente. Remova essa barra final e o mesmo pedido chega como /app/status. Remover o prefixo costuma quebrar as ligações para os recursos da própria aplicação, que permanecem absolutas e depois retornam 404 na raiz do site. Por isso, uma aplicação com uma definição de caminho base é melhor servida pela forma que transmite o caminho.
Por que o log da minha aplicação regista 127.0.0.1 como o endereço IP de todos os visitantes?
Porque a ligação recebida pela aplicação vem efetivamente do nginx através do endereço de loopback. O endereço do visitante só chega à aplicação num cabeçalho que definiu: proxy_set_header X-Real-IP $remote_addr; para um valor único e proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; para a cadeia acrescentada. A aplicação também tem de ser configurada para confiar nesses cabeçalhos. Tenha em atenção que um cliente pode enviar o seu próprio X-Forwarded-For. Por isso, quando o nginx é o servidor de edge, substitua-o por $remote_addr em vez de o acrescentar.
Preciso de TLS na ligação entre o nginx e a minha aplicação?
Não, quando a aplicação é executada no mesmo servidor e está associada a 127.0.0.1, porque esse tráfego nunca sai da máquina. Termine o TLS no nginx, mantenha proxy_pass em HTTP simples através do loopback e envie X-Forwarded-Proto $scheme para que a aplicação saiba que o visitante usou HTTPS. Se o backend estiver noutro host, através de uma rede que não controla, esse salto precisa da sua própria proteção: HTTPS até ao backend ou um túnel privado entre as duas máquinas.