Nginx como reverse proxy: SSL, load balancing e caching

Colocar uma aplicacao web em producao expondo diretamente a porta do Node.js, Python ou Go para a internet publica e pedir para ter dor de cabeca. Servidores de aplicacao sao excelentes para processar regras de negocio, lidar com I/O assincrono e responder requisicoes dinamicas. No entanto, eles raramente foram desenhados para suportar com eficiencia o peso de negociacao TLS pesada, ataques lentos de rede, buffering de uploads gigantescos ou distribuicao de arquivos estaticos.

E aqui que o Nginx entra como um divisor de aguas. Criado no inicio dos anos 2000 por Igor Sysoev para resolver o famoso problema C10k (sustentar dez mil conexoes simultaneas em uma unica maquina), o Nginx usa uma arquitetura assincrona orientada a eventos. Em vez de abrir uma thread pesada para cada conexao TCP, ele processa milhares de clientes com consumo minimo de memoria.

Configurar o Nginx como reverse proxy transforma a arquitetura de qualquer aplicacao. Ele funciona como uma camada de blindagem e performance: assume a terminacao SSL, distribui carga entre instancias, aplica cache de respostas e protege o backend contra abusos de requisicoes.

Anatomia basica de um reverse proxy

Para quem gerencia servidores Linux, a primeira barreira com o Nginx costuma ser a estrutura de arquivos de configuracao. O arquivo principal geralmente fica em /etc/nginx/nginx.conf, mas a boa pratica para manter multiplos servicos organiza os blocos em /etc/nginx/sites-available/ e cria links simbolicos em /etc/nginx/sites-enabled/.

A diretiva central que transforma o Nginx em um proxy reverso e a proxy_pass. Ela diz ao servidor web para pegar a requisicao recebida na porta 80 ou 443 e repassa-la para um servico interno escutando em outra porta ou em um socket UNIX local.

# /etc/nginx/sites-available/meuapp.conf

server {
    listen 80;
    server_name api.exemplo.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }
}

O bloco acima faz muito mais do que apenas repassar trafego. Ao configurar proxy_set_header, garantimos que a aplicacao backend saiba quem realmente fez a requisicao.

Se voce nao passar X-Real-IP e X-Forwarded-For, todos os logs do seu framework em Node.js ou FastAPI vao registrar 127.0.0.1 como origem de todo o trafego. Isso inviabiliza analises de seguranca, auditoria ou bloqueios por IP no nivel da aplicacao.

As diretivas Upgrade e Connection garantem compatibilidade imediata com WebSockets. Se a sua aplicacao mantem conexoes persistentes em tempo real, sem esses headers o Nginx vai cortar o handshake e derrubar a comunicacao.

Blindando a conexao com SSL e Let's Encrypt

Subir um servico sem criptografia hoje e inviavel. Alem da questao basica de privacidade, navegadores modernos penalizam conexoes puras em HTTP, e APIs bloqueiam chamadas inseguras por padrao.

A combinacao de Nginx com o Certbot da Electronic Frontier Foundation (EFF) torna a emissao e renovacao de certificados SSL/TLS da Let's Encrypt praticamente automatica no Linux.

Para instalar o Certbot no Ubuntu, Debian ou em distribuicoes baseadas em Arch Linux, basta utilizar o gerenciador de pacotes padrao e acionar o plugin proprio do Nginx:

# Instalacao e emissao automatica
sudo certbot --nginx -d api.exemplo.com

O Certbot le a configuracao existente do Nginx, valida o desafio ACME contra o dominio publico, baixa os certificados e altera o arquivo de configuracao automaticamente.

No entanto, entender o que acontece por baixo do pano evita surpresas quando precisamos customizar cifras ou aplicar headers de seguranca mais rigidos. Uma configuracao SSL solida no Nginx deve se parecer com isto:

server {
    listen 80;
    server_name api.exemplo.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.exemplo.com;

    ssl_certificate /etc/letsencrypt/live/api.exemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.exemplo.com/privkey.pem;

    # Protocolos seguros e cifras modernas
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

    # Otimizacao de sessao TLS
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # Seguranca de transporte
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    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;
    }
}

O primeiro bloco escuta na porta 80 e redireciona qualquer requisicao para HTTPS com o codigo 301 (redirecionamento permanente).

No bloco principal, habilitamos o http2 para multiplexacao de conexoes sobre uma unica sessao TCP, reduzindo a latencia de carregamento. O parametro ssl_session_cache armazena dados de negociacao TLS na memoria RAM do Nginx, evitando que clientes recorrentes precisem refazer todo o handshake criptografico a cada chamada.

Distribuicao de trafego com Load Balancing

Quando a aplicacao cresce ou quando precisamos rodar processos em paralelo com ferramentas como PM2 ou containers Docker, manter apenas uma instancia do backend cria um ponto unico de falha e gargalo de CPU.

O Nginx resolve isso de forma nativa por meio do bloco upstream. Ele permite definir um pool de servidores e distribuir as requisicoes recebidas entre eles.

upstream node_backend {
    # Algoritmo padrao: Round Robin
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003 max_fails=3 fail_timeout=30s;
    
    # Servidor de contingencia
    server 127.0.0.1:3004 backup;
}

server {
    listen 443 ssl http2;
    server_name api.exemplo.com;

    # ... certificados SSL ...

    location / {
        proxy_pass http://node_backend;
        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;
        
        # Manter conexoes ativas com o backend
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Nessa configuracao, o Nginx usa Round Robin por padrao, enviando a primeira requisicao para a porta 3001, a segunda para a 3002 e a terceira para a 3003. Se a porta 3003 falhar tres vezes seguidas dentro de 30 segundos, ela e temporariamente removida da fila. O servidor na porta 3004 so recebera trafego se todos os outros tres estiverem fora do ar.

Existem outros metodos de distribuicao dependendo do perfil da sua carga:

  • least_conn: envia a requisicao para o servidor com menos conexoes ativas no momento. Ideal para rotas que executam processamentos lentos e desiguais.
  • ip_hash: garante que requisicoes vindas do mesmo endereco IP sempre caiam na mesma instancia backend. Util quando a aplicacao ainda depende de sessoes em memoria local.

Para manter alta eficiencia no upstream, e recomendavel configurar o bloco keepalive dentro do upstream, permitindo que o Nginx reutilize conexoes TCP abertas diretamente com as aplicacoes Node.js ou Python.

Caching de assets e respostas no proxy

Fazer o backend responder requisicoes identicas de dados estaticos ou consultas pesadas de banco de dados e um desperdicio claro de recursos. O Nginx pode atuar como um cache reverso em disco ou memoria compartilhada, respondendo antes mesmo de incomodar o Node.js.

Configurar cache envolve duas etapas: declarar a zona de memoria no contexto http e aplicar a regra dentro do bloco location.

# Fora do bloco server, no contexto http
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=API_CACHE:10m max_size=1g inactive=60m use_temp_path=off;

server {
    listen 443 ssl http2;
    server_name api.exemplo.com;

    # ... certificados SSL ...

    location /static/ {
        proxy_pass http://node_backend;
        proxy_cache API_CACHE;
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_lock on;

        # Header util para debug
        add_header X-Cache-Status $upstream_cache_status;
    }
}

A diretiva proxy_cache_path define onde os arquivos cacheados vao morar no disco do Linux. keys_zone=API_CACHE:10m cria uma area de memoria de 10 megabytes para armazenar as chaves de busca e metadados, o que e suficiente para dezenas de milhares de rotas indexadas.

O parametro proxy_cache_use_stale e extremamente util para manter alta disponibilidade. Se o backend travar ou demorar para responder uma atualizacao, o Nginx continua servindo a versao antiga que estava no cache enquanto tenta restabelecer a conexao.

Ao inspecionar o header X-Cache-Status nas respostas HTTP da sua aplicacao, voce vera valores como HIT (servido direto do cache do Nginx), MISS (buscado no backend e salvo no cache) ou BYPASS.

Se voce precisa servir arquivos puramente estaticos gerados por um build de frontend (como Vite ou Next.js exportado), e ainda mais performatico deixar o proprio Nginx ler os arquivos direto do disco com try_files, ignorando o proxy completamente para essas extensoes:

location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
    root /var/www/meuapp/dist;
    expires 30d;
    add_header Cache-Control "public, no-transform";
    access_log off;
}

Protegendo a API com Rate Limiting

Deixar uma rota de autenticacao (/api/login) ou endpoints criticos abertos sem restricao de frequencia e um convite para ataques de forca bruta e scraping agressivo. O Nginx implementa rate limiting atraves do algoritmo Leaky Bucket (balde furado), processando requisicoes com uma taxa constante e descartando ou enfileirando o excesso.

Para configurar, definimos o espaco de memoria e a taxa de requisicoes por segundo (r/s) ou por minuto (r/m):

# No contexto http
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_general:10m rate=30r/s;

server {
    listen 443 ssl http2;
    server_name api.exemplo.com;

    # ... configuracoes anteriores ...

    # Rota sensivel de autenticacao
    location /api/auth/login {
        limit_req zone=login_limit burst=2 nodelay;
        limit_req_status 429;

        proxy_pass http://node_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # API geral
    location /api/ {
        limit_req zone=api_general burst=10 nodelay;
        limit_req_status 429;

        proxy_pass http://node_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

O uso de $binary_remote_addr em vez de $remote_addr e uma otimizacao classica de memoria no Linux: ele armazena o endereco IPv4 em apenas 4 bytes, ocupando muito menos espaco na zona compartilhada de 10 megabytes.

O parametro burst=10 nodelay permite que o cliente envie picos momentaneos de ate 10 requisicoes alem da taxa base sem sofrer atraso forçado. Se o volume exceder esse limite de explosao, o Nginx corta a conexao imediatamente e retorna o codigo HTTP 429 (Too Many Requests).

Isso protege a aplicacao de sobrecargas antes mesmo do Node.js gastar ciclos de CPU parseando o corpo JSON da requisicao.

Validacao e deploy sem downtime

Um erro frequente ao mexer em configuracoes do Nginx e reiniciar o servico bruscamente, derrubando todas as conexoes abertas caso exista um erro de sintaxe em algum arquivo importado.

O fluxo profissional de manutencao no terminal Linux deve sempre seguir dois passos: testar a configuracao e recarregar os processos de forma graciosa.

# 1. Testa a sintaxe de todos os arquivos de configuracao
sudo nginx -t

# 2. Se a sintaxe estiver OK, recarrega sem fechar conexoes ativas
sudo systemctl reload nginx

O comando reload instrui o processo mestre do Nginx a reler os arquivos de configuracao, iniciar novos processos workers com as alteracoes e desligar os antigos somente apos eles terminarem de responder as requisicoes em andamento. Nao ha perda de pacotes e a troca ocorre de forma invisivel para os usuarios.

Takeaways praticos

  • Nunca exponha servidores de aplicacao (Node.js, Flask, FastAPI) diretamente para a internet publica; utilize o Nginx na borda.
  • Sempre repasse os cabecalhos Host, X-Real-IP, X-Forwarded-For e X-Forwarded-Proto nas diretivas de proxy para manter a rastreabilidade dos clientes no backend.
  • Habilite HTTP/2 junto com o SSL para permitir transmissao multiplexada de dados na mesma conexao TCP.
  • Isole endpoints sensiveis de login e busca pesada usando zonas de limit_req para mitigar ataques de forca bruta e scraping.
  • Deixe o Nginx servir arquivos estaticos e assets cacheados direto do disco, poupando threads e ciclos de CPU do servidor backend.
  • Use nginx -t antes de qualquer reinicializacao e prefira systemctl reload nginx para evitar quedas no ambiente de producao.

Glossário do post

node
Node.js: runtime JavaScript fora do navegador, baseado no motor V8, usado principalmente para construir servidores e ferramentas de linha de comando.
python
Linguagem de programação de alto nível e sintaxe legível, popular em scripts, automação, dados e IA.
Go
Linguagem compilada da Google, conhecida por concorrência simples (goroutines) e performance, muito usada em serviços backend.
cache
Armazenamento temporário de dados já calculados ou buscados para acelerar acessos futuros e reduzir carga no servidor.
http
Hypertext Transfer Protocol: o protocolo de comunicação usado entre navegadores e servidores web.
ram
Random Access Memory: memória volátil de acesso rápido usada para dados em uso durante a execução de programas.
cpu
Central Processing Unit: o processador que executa instruções do programa, o 'cérebro' do computador.
api
Application Programming Interface: conjunto de regras que permite a um programa conversar com outro, expondo dados e funcionalidades de forma padronizada.
json
JavaScript Object Notation: formato leve de troca de dados baseado em pares chave/valor, legível para humanos e máquinas.
Ismael Douglas

Sobre o autor

Ismael Douglas

Desenvolvedor Full Stack apaixonado por construir produtos web rápidos e bem projetados. Escrevo sobre o que aprendo no caminho — código, infra e boas práticas.

Veja meus projetos no portfólio →