O que o Nginx realmente é
O Nginx (pronuncia-se “engine-x”) é um servidor web construído em torno de uma arquitetura assíncrona e orientada a eventos. Em vez de utilizar uma thread ou processo por conexão, um pequeno número de processos worker gerencia milhares de conexões simultaneamente ao reagir a eventos. Esse design é o motivo pelo qual ele consegue ficar à frente de uma aplicação lenta sem travar, e por que se tornou a porta de entrada padrão para a web moderna.
Ele desempenha três papéis que são fáceis de confundir. Ele é um servidor web estático, entregando arquivos do disco com eficiência. É um reverse proxy, encaminhando requisições para outro servidor e retransmitindo a resposta. E é um load balancer, distribuindo as requisições entre um pool de backends. À frente de um app em Node.js, Python, Go ou PHP, você usará as três funções. O Nginx não faz parte da sua aplicação: ele é um processo separado, com sua própria configuração, ciclo de vida e logs.
Por que colocá-lo na frente do seu app
Você poderia vincular o Node diretamente à porta 80. Muitas pessoas fazem isso em tutoriais, e funciona até que pare de funcionar. Colocar o Nginx na frente garante um conjunto de funcionalidades que são tediosas ou perigosas de implementar dentro de uma aplicação.
TLS termination. O Nginx detém o certificado, realiza o handshake e descriptografa. Seu app fala HTTP simples no localhost e nunca precisa gerenciar a renovação de certificados. O Certbot pode até reescrever a configuração para você.
Arquivos estáticos, compressão e cache. O Nginx serve arquivos com sendfile e cache de kernel, comprime com gzip ou brotli, e pode repetir uma resposta armazenada sem sequer tocar no upstream.
Rate limiting e limites de requisição. Abusos são interrompidos na borda, antes que consumam um pool de conexões ou uma query no banco de dados.
Um único ponto de entrada. Um único endereço público e certificado podem servir a vários serviços, roteados por hostname ou path. Você pode mover um app para outra porta ou host sem alterar o DNS.
Zero-downtime deploys. Um upstream pode esvaziar um backend enquanto ele reinicia, e o reload da configuração é graceful. O Node exposto diretamente derruba todas as conexões em andamento ao reiniciar.
O modelo de configuração
A configuração do Nginx é uma árvore de contextos e diretivas. Uma diretiva é uma configuração que termina em ponto e vírgula; um contexto é um bloco delimitado por chaves que contém mais diretivas. As diretivas são herdadas para baixo, e o contexto mais específico prevalece.
# /etc/nginx/nginx.conf (main context)
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_types text/css application/javascript application/json;
# site configs are pulled in here
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Os contextos com os quais você trabalhará são:
- main — configurações globais, como
usereworker_processes. - events — como as conexões são manipuladas.
- http — tudo relacionado a HTTP: tipos MIME, logging, compressão e os includes que importam os arquivos do site.
- server — um host virtual, selecionado por
listeneserver_name. - location — um caminho dentro de um server, onde as requisições são efetivamente processadas.
No Debian e Ubuntu, a convenção é manter um arquivo por site em /etc/nginx/sites-available/ e criar symlinks dos sites ativos em /etc/nginx/sites-enabled/. Isso oferece uma maneira fácil de desativar um site sem excluí-lo.
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
sudo rm /etc/nginx/sites-enabled/app
Diretivas não são uma linguagem de script. Não existem loops ou variáveis no sentido usual — apenas map, if (usados com moderação) e includes. Se um problema parecer exigir fluxo de controle, a resposta geralmente é um map ou uma alteração na aplicação.
Seu primeiro server block
Um bloco server é um virtual host. Ele escuta em uma porta, responde por um ou mais nomes e contém blocos location.
server {
listen 80;
server_name app.example.com www.app.example.com;
root /var/www/app/dist;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
O server_name é comparado com o header Host. O primeiro server block de uma porta é o default server e captura requisições cujo host não coincida com nenhum outro; por isso, vale a pena torná-lo um catch-all explícito que retorne 444 ou uma página de manutenção, em vez de expor um site indesejado.
A ordem de correspondência é importante e costuma confundir as pessoas. Blocos location são testados primeiro como correspondências exatas (=), depois a correspondência de prefixo mais longa e, por fim, regexes (~ e ~*). Um prefixo ^~ instrui o Nginx a parar e usar aquele prefixo sem verificar regexes. Quando o comportamento parece incorreto, geralmente é por causa dessa ordenação.
location = /favicon.ico { log_not_found off; access_log off; }
location ^~ /assets/ { expires 1y; }
location ~* \.(jpg|png|css|js)$ { expires 30d; }
location / { try_files $uri /index.html; }
Fazendo proxy para um app Node.js
A diretiva principal é proxy_pass. Aponte-a para a sua aplicação e o Nginx se tornará um reverse proxy.
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 30s;
proxy_connect_timeout 5s;
}
}
Definir um bloco upstream em vez de colocar o endereço inline não serve apenas para múltiplos servidores. É isso que permite configurar o keepalive, fazendo com que o Nginx reutilize as conexões com o backend em vez de abrir uma nova conexão TCP para cada requisição. O proxy_http_version 1.1 é necessário para o keepalive e para WebSockets.
Os timeouts merecem atenção. O proxy_connect_timeout limita quanto tempo o Nginx espera para estabelecer a conexão; o proxy_read_timeout limita quanto tempo ele espera pelo próximo byte do upstream. O timeout de leitura deve ser maior que a sua resposta legítima mais lenta, caso contrário, o Nginx retornará um 504 enquanto o app ainda estiver processando.
A regra da barra final (trailing-slash) é a armadilha clássica. proxy_pass http://app; encaminha o caminho completo sem alterações. proxy_pass http://app/; substitui o prefixo da location correspondente por /. Dentro de location /api/, o primeiro envia /api/users e o segundo envia /users. Escolha com critério e verifique o log do upstream.
Encaminhamento de headers e por que sua aplicação precisa disso
Por padrão, o upstream enxerga a requisição como vinda do Nginx: o endereço do cliente é 127.0.0.1, o scheme é http, e o header Host pode estar ausente. Isso quebra logs, redirecionamentos, cookies e rate limiting dentro da aplicação.
location / {
proxy_pass http://app;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
Cada header tem sua função:
- Host preserva o domínio original, permitindo que a aplicação construa URLs absolutas e virtual hosts corretamente.
- X-Real-IP é o endereço imediato do cliente.
- X-Forwarded-For anexa a uma cadeia;
$proxy_add_x_forwarded_formantém as entradas existentes e adiciona o cliente atual. Use a primeira entrada como o cliente original, mas nunca confie nela cegamente. - X-Forwarded-Proto informa à aplicação se o usuário se conectou via HTTPS, o que corrige loops de redirecionamento e bugs de secure-cookie.
- X-Forwarded-Host e X-Forwarded-Port são úteis quando o Nginx escuta em uma porta não padrão.
Seu framework deve ser configurado para confiar neste proxy. No Express, app.set("trust proxy", 1) faz com que req.ip e req.protocol leiam os valores encaminhados. Se pular essa etapa, cada requisição parecerá vir do localhost, o que quebrará silenciosamente a geolocalização e o rate limiting por IP.
Servindo arquivos estáticos e o fallback de SPA
O Nginx é excelente com arquivos estáticos, então deixe que ele cuide disso. Uma divisão comum é servir o frontend buildado a partir do disco e fazer o proxy apenas da API.
server {
listen 80;
server_name app.example.com;
root /var/www/app/dist;
location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /api/ {
proxy_pass http://app;
proxy_set_header Host $host;
}
location / {
try_files $uri /index.html;
}
}
Para um single-page app, try_files $uri /index.html é o fallback que faz o roteamento no client-side funcionar: se o caminho solicitado não for um arquivo, retorne index.html e deixe que o router resolva. Para uma API, o mesmo padrão com =404 é melhor, pois retornar HTML para um endpoint inexistente esconde bugs.
Assets com hashes de conteúdo em seus nomes podem ser cacheados para sempre — immutable e uma expiração de um ano são seguros, pois qualquer alteração gera um novo nome de arquivo. index.html nunca deve ser cacheado dessa forma, caso contrário, os usuários continuarão carregando uma versão antiga do app.
TLS com Let’s Encrypt e certbot
O Certbot obtém certificados gratuitos do Let’s Encrypt e pode configurar o Nginx automaticamente. Instale o plugin, execute-o uma vez por domínio e ele cuidará do restante.
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -d www.app.example.com
sudo certbot renew --dry-run
O Certbot escreve os caminhos dos certificados no seu server block e instala um timer de renovação. Os certificados duram 90 dias; a renovação é automática, mas só funciona se a verificação de renovação conseguir alcançar seu servidor na porta 80, portanto, não a bloqueie totalmente no firewall.
O bloco resultante fica assim, com um servidor HTTP que redireciona e um servidor HTTPS que finaliza a conexão.
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
TLSv1.2 e TLSv1.3 são os únicos protocolos que vale a pena habilitar hoje em dia. O HSTS informa aos navegadores que recusem HTTP simples por um ano, então habilite-o apenas quando tiver certeza de que o HTTPS funciona em todos os lugares — é difícil de reverter.
HTTP/2 e HTTP/3
O HTTP/2 multiplexa várias requisições em uma única conexão e é habilitado por servidor. No Nginx moderno (1.25.1 e posteriores), a diretiva é http2 on;. Em versões mais antigas, trata-se de uma flag na linha listen: listen 443 ssl http2;.
server {
listen 443 ssl;
http2 on;
# ...
}
O HTTP/3 roda sobre QUIC e UDP. Ele requer um build com o módulo QUIC, uma linha listen 443 quic reuseport; separada e um header Alt-Svc: h3=":443"; ma=86400 para anunciá-lo. Trate-o como uma otimização para adicionar assim que o HTTP/2 estiver estável, e não como um primeiro passo. Os navegadores negociam ambos via TLS automaticamente, portanto não há nada a alterar no seu app.
Métodos de balanceamento de carga
Um bloco upstream define um pool, e o método de balanceamento decide qual servidor recebe a próxima requisição. O padrão é o round-robin.
upstream app {
least_conn;
server 10.0.0.11:3000 weight=2 max_fails=3 fail_timeout=15s;
server 10.0.0.12:3000;
server 10.0.0.13:3000 backup;
keepalive 64;
}
- Round-robin (padrão) alterna entre os servidores. Simples e eficiente quando as requisições têm custos semelhantes.
least_connenvia para o servidor com o menor número de conexões ativas. Melhor quando a duração das requisições varia muito, o que é comum em APIs.ip_hashvincula um cliente a um único servidor através do hash do endereço. Útil para sessões em memória, porém com o custo de uma carga desigual e a perda da persistência quando um servidor falha.weightdireciona o tráfego preferencialmente para uma máquina mais potente ou durante um canary.backupmarca um servidor para receber tráfego apenas quando os primários estiverem fora do ar.
max_fails e fail_timeout oferecem verificação de saúde passiva: após max_fails falhas dentro de fail_timeout, o Nginx marca o servidor como indisponível durante aquele intervalo. O proxy_next_upstream então decide quais falhas devem ser repetidas em outro backend.
proxy_next_upstream error timeout http_502 http_503 http_504;
Tentar novamente (retry) é poderoso, mas não é gratuito: só faz sentido para requisições idempotentes. Não repita cegamente um POST que possa já ter alterado o estado, a menos que o upstream seja idempotente por design.
Health checks e tratamento de falhas
O Nginx OSS possui verificações passivas; verificações ativas de saúde (health_check com match) exigem o Nginx Plus. Verificações passivas combinadas com um endpoint no nível da aplicação costumam ser suficientes para a maioria dos times: exponha /healthz que verifique as dependências necessárias — banco de dados, cache — e retorne rapidamente, então aponte a liveness probe da sua plataforma para ele. Use isso para controlar deploys, iniciando um novo backend, aguardando até que ele esteja saudável e, então, removendo o antigo do upstream.
Como os reloads de configuração são suaves (graceful), o padrão para zero-downtime é editar o upstream para apontar para a nova instância, nginx -t, fazer o reload e, em seguida, drenar e parar a instância antiga.
Cache com proxy_cache
proxy_cache armazena as respostas do upstream no disco e as serve sem precisar contatar o backend. Para endpoints com alta carga de leitura que mudam lentamente, este é o maior ganho individual disponível na edge.
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=app:10m max_size=1g inactive=60m;
server {
listen 80;
server_name app.example.com;
location /api/public/ {
proxy_pass http://app;
proxy_cache app;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10s;
proxy_cache_valid 404 1m;
proxy_cache_bypass $http_authorization $cookie_session;
proxy_no_cache $http_authorization;
add_header X-Cache-Status $upstream_cache_status;
}
}
proxy_cache_valid define por quanto tempo cada código de status é cacheado. proxy_cache_bypass ignora o cache para requisições que contenham um header de sessão ou autorização; proxy_no_cache evita o armazenamento de suas respostas, garantindo que os dados privados de um usuário nunca sejam servidos a outro. O header X-Cache-Status (HIT, MISS, BYPASS, EXPIRED) é a maneira mais rápida de provar que o cache está funcionando.
Dois alertas. Cacheie apenas respostas que sejam seguras para compartilhar — dados públicos e não personalizados — e certifique-se de que o upstream envie um header Cache-Control compatível com a sua configuração. Uma cache key que ignore uma variante (um header de idioma, um parâmetro de query) servirá alegremente o conteúdo errado.
Rate limiting com limit_req
O rate limiting protege endpoints de login, busca e APIs públicas contra abusos e floods acidentais. Ele é definido no contexto http e aplicado em uma location.
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_status 429;
server {
listen 80;
server_name app.example.com;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
}
location = /api/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
}
}
A chave da zona geralmente é $binary_remote_addr (o IP do cliente), e a taxa é definida em requisições por segundo ou por minuto. O burst permite picos curtos, e o nodelay processa o burst imediatamente em vez de espaçá-lo — que é o comportamento desejado para endpoints interativos. Sem o nodelay, as requisições excedentes são atrasadas, o que pode deixar a UI com a sensação de lentidão em vez de simplesmente retornar 429.
Atrás de outro proxy ou de uma CDN, o $binary_remote_addr pode ser o endereço do proxy, fazendo com que o limiter trate todos como um único cliente. Corrija isso configurando set_real_ip_from e real_ip_header para que o Nginx veja o cliente real. Além disso, retorne 429 em vez do padrão 503 para que os clientes possam distinguir o rate limiting de uma queda no serviço.
Upgrade de WebSocket
Uma conexão WebSocket começa como uma requisição HTTP com um header Upgrade e, em seguida, torna-se um stream bidirecional de longa duração. Para fazer o proxy dessa conexão, são necessários os headers de upgrade e um valor Connection que depende da requisição, por isso utiliza-se um map.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
location /ws/ {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
O timeout de leitura longa é fundamental: um WebSocket inativo com um timeout curto é fechado pelo Nginx, e o cliente percebe uma desconexão misteriosa. Note que o HTTP/2 não transporta WebSocket da mesma maneira; os navegadores abrem uma conexão HTTP/1.1 separada para o upgrade, a qual o Nginx gerencia automaticamente na mesma porta.
Executando Nginx no Docker
A imagem oficial do nginx é uma maneira conveniente de distribuir configurações e arquivos estáticos com seu app. A única regra é que o processo principal do container deve permanecer em primeiro plano, por isso o comando termina com daemon off;.
# Dockerfile
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/app.conf
COPY dist/ /usr/share/nginx/html/
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
No Compose, o serviço do app é acessível pelo nome do serviço, então o host upstream é simplesmente app. O Nginx inicia antes do app estar pronto, portanto, adicione um healthcheck e dependa dele, ou deixe o Nginx tentar novamente.
# docker-compose.yml
services:
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/app.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
app:
condition: service_healthy
app:
build: .
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
interval: 5s
retries: 10
Note o uso de expose em vez de ports no app: ele deve ser acessível apenas dentro da rede do Compose, nunca diretamente do host. O upstream da configuração do nginx torna-se server app:3000;.
Testes, recarregamento e alterações de configuração com zero-downtime
Toda alteração de configuração deve seguir o mesmo ciclo: validar, recarregar, verificar. Um reload é gracioso; um restart não é.
sudo nginx -t # test the configuration
sudo nginx -T # test and dump the full effective config
sudo systemctl reload nginx # graceful, no dropped connections
sudo systemctl status nginx
curl -I https://app.example.com
nginx -T é subutilizado e extremamente útil: ele imprime a configuração mesclada exatamente como o Nginx a enxerga, o que resolve discussões sobre herança e includes. Se o -t falhar, o erro indica o arquivo e a linha.
Recarregar mantém um shell aberto em uma segunda sessão, para que uma configuração incorreta não bloqueie seu acesso. Em um container, o equivalente é enviar SIGHUP para o processo master ou executar nginx -s reload. Nunca reinicie um Nginx de produção com tráfego para aplicar uma alteração de configuração quando um reload resolve.
Melhores práticas
- Execute
nginx -tantes de cada reload; nunca edite e reinicie cegamente. - Mantenha um arquivo por site e ative-o com um symlink para que as alterações sejam reversíveis.
- Defina um bloco
upstreamcomkeepaliveem vez de colocar o endereço do backend inline. - Sempre encaminhe
Host,X-Real-IP,X-Forwarded-ForeX-Forwarded-Proto, e configuretrust proxyno app. - Sirva assets estáticos e bundles com hash a partir do disco com headers de cache longos e imutáveis.
- Redirecione HTTP para HTTPS e ative o HSTS apenas após o HTTPS estar comprovadamente funcionando.
- Faça cache apenas de respostas públicas e não personalizadas, e nunca crie a chave de um cache sem suas variantes.
- Aplique rate limit em endpoints de login e públicos, e retorne 429 em vez de 503.
- Use timeouts longos para locações de WebSocket e curtos para health checks.
- Mantenha secrets e certificados fora das imagens; monte-os como read-only em tempo de execução.
Erros comuns
- Pular o
nginx -te derrubar o site com um erro de sintaxe. - Esquecer o
X-Forwarded-Protoe acabar debugando um loop infinito de redirecionamento HTTPS. - Não habilitar o
trust proxyno app, fazendo com que cada requisição pareça ter vindo do localhost. - Confundir
proxy_passcom e sem a barra final (trailing slash) e receber erros 404. - Configurar o
proxy_read_timeoutcom um tempo menor do que uma resposta lenta legítima e retornar erros 504. - Cachear respostas personalizadas e servir os dados de um usuário para outro.
- Fazer o proxy de WebSockets sem o
mapde upgrade, resultando na queda imediata das conexões. - Deixar o servidor padrão como o primeiro bloco de site e expor um app indesejado.
- Habilitar HSTS antes que o HTTPS funcione em todos os lugares e bloquear o acesso dos usuários por um ano.
- Expor o serviço do app com
portsno Compose em vez deexpose.
Próximos passos
O Nginx é a porta de entrada, e o guia de HTTP / HTTPS explica o protocolo que ele encerra e encaminha. Para executá-lo ao lado do seu app de forma reprodutível, leia sobre Docker & Deployment, e para manter a saúde do host onde ele está rodando, revise a seção de Linux. Quando você estiver pronto para colocá-lo na internet pública com certificados, DNS e autoscaling, o guia de Cloud Deployment une todas essas peças.