Um binário Zig que responde HTTP sozinho é só metade do caminho para produção. No mundo real, quase nenhum serviço fica exposto diretamente na porta 443. Na frente dele costuma existir um proxy reverso — na maioria das vezes Nginx — responsável por terminação TLS, compressão, cache de estáticos, rate limiting de borda, roteamento entre várias réplicas e isolamento contra a internet hostil. Colocar um serviço Zig atrás de Nginx é simples, mas tem um conjunto de armadilhas recorrentes: timeout do upstream menor que o do cliente, WebSockets sem Upgrade, balanceamento para um socket já morto, headers de confiança ignorados e TLS terminado duas vezes.
Este guia mostra o desenho que se repete quando um binário Zig precisa ficar por trás de Nginx em produção: upstream, terminação TLS, balanceamento de carga, WebSockets, timeouts coerentes, backpressure, headers de segurança e troubleshooting dos erros mais comuns. O foco não é defender uma arquitetura específica, e sim deixar explícito o contrato entre o proxy e o serviço Zig para que a operação fique previsível.
Este artigo complementa servidor HTTP em Zig para produção, servidor HTTP da stdlib, Zig com systemd: serviço Linux em produção, TLS, HTTPS e mTLS em Zig, rate limiting com token bucket e Server-Sent Events em Zig. A mentalidade é a mesma que aparece nesses guias: fronteira pequena, contrato explícito e comportamento previsível sob falha.
Por que um proxy reverso na frente de Zig
Zig consegue servir HTTP com performance excelente e controle fino de memória, mas isso não significa que o binário deva assumir todas as responsabilidades de borda. Existem tarefas para as quais Nginx (ou HAProxy, Traefik, Caddy) foi construído e auditado por décadas:
- Terminação TLS com renovação automática de certificados (Let’s Encrypt, ACM, mTLS interno).
- HTTP/2 e HTTP/3 sem custo de implementação no serviço.
- Compressão gzip/brotli com buffers ajustados.
- Cache de estáticos (CSS, JS, fontes, imagens) sem tocar no aplicativo.
- Rate limiting e IP allowlist/denylist na borda, antes do tráfego chegar ao upstream.
- Balanceamento de carga entre várias réplicas do mesmo binário.
- Isolamento de rede: o serviço Zig escuta apenas em
127.0.0.1ou num unix socket privado.
A divisão de responsabilidades é o ponto. O binário Zig faz o que ele sabe fazer bem — lógica de negócio, I/O explícito, alocação controlada, baixa latência — e o proxy assume a parte de borda, onde décadas de patches de segurança e otimizações já foram pagas.
O erro comum é o oposto: achar que Zig “como é rápido” dispensa o proxy. O resultado é um serviço exposto diretamente, sem renovação de TLS, sem cache, sem redundância, sem proteção contra tráfego malicioso. Não é uma questão de velocidade; é uma questão de operação.
O upstream: como o Nginx enxerga o serviço Zig
A peça central de qualquer bloco Nginx que faz proxy reverso é o upstream. Ele descreve para onde o proxy vai mandar a requisição. Para um serviço Zig rodando na porta 8080 da mesma máquina, o upstream é direto:
upstream zig_backend {
server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
keepalive 32;
}
Dois detalhes merecem atenção. Primeiro, max_fails e fail_timeout definem o comportamento quando uma réplica para de responder: depois de 3 falhas em 10 segundos, o Nginx considera o upstream indisponível e, se houver outras réplicas, manda o tráfego para elas. Sem isso, uma répia travada continua recebendo requisições até um humano intervir. Segundo, keepalive 32 mantém um pool de conexões TCP reutilizadas para o upstream, evitando o custo de abrir um socket novo a cada requisição. Isso só funciona se o serviço Zig também souber manter conexões keep-alive; a stdlib std.http.Server suporta, mas vale validar com um teste de carga simples.
Quando existem várias réplicas (típico em deploy com systemd em mais de uma instância, ou em containers orquestrados), o upstream lista todas:
upstream zig_backend {
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.13:8080 max_fails=3 fail_timeout=10s backup;
keepalive 64;
}
A diretiva least_conn encaminha cada nova requisição para a réplica com menos conexões ativas. backup marca uma réplica que só recebe tráfego quando todas as outras falham — útil para manter uma instância de reserva quente.
Proxy reverso e repasse de headers
O bloco server faz o casamento entre a requisição externa e o upstream. A regra de ouro é: o serviço Zig precisa saber de onde a requisição veio (IP real, host original, esquema original), porque sem isso ele toma decisões erradas sobre redirecionamentos, logs e CSRF. O Nginx entrega essas informações em headers X-Forwarded-* e X-Real-IP:
server {
listen 443 ssl http2;
server_name api.exemplo.com.br;
ssl_certificate /etc/letsencrypt/live/api.exemplo.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.exemplo.com.br/privkey.pem;
location / {
proxy_pass http://zig_backend;
proxy_http_version 1.1;
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_connect_timeout 2s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
}
}
Vale revisar cada parte. proxy_http_version 1.1 é obrigatório para manter a conexão keep-alive com o upstream e também para WebSockets. Os headers X-Forwarded-* precisam ser repassados explicitamente — por padrão o Nginx não os envia. proxy_connect_timeout é o tempo limite para abrir a conexão TCP com o upstream; se o serviço Zig demora mais que 2 segundos para aceitar a conexão, algo está muito errado e o Nginx devolve 502. proxy_send_timeout e proxy_read_timeout são os limites entre duas operações de leitura/escrita — não é o tempo total da requisição, e sim o tempo máximo entre dois pacotes.
No lado Zig, o serviço precisa ler esses headers para reconstruir a URL original. Sem isso, qualquer redirecionamento que o serviço emita vai apontar para http:// em vez de https://, quebrando fluxos de login e OAuth. O padrão seguro é confiar em X-Forwarded-Proto apenas quando a requisição veio do Nginx (caso contrário, qualquer cliente pode forjar o header).
Load balancing e coerência de timeout
O erro de timeout mais comum em deploy com proxy é a incoerência entre cliente, proxy e serviço. Se o cliente espera até 60 segundos, o proxy corta em 30 e o serviço corta em 10, todo mundo descobre o problema só quando aparece. A regra prática é: timeout do serviço ≤ timeout do proxy ≤ timeout do cliente. Assim, quem cancela a requisição primeiro é sempre o serviço Zig, que sabe o que fazer com o cancelamento (liberar recursos, registrar métrica, devolver erro estruturado).
Para rotas que sabidamente demoram mais — upload de arquivos, exportação de relatórios, webhooks de processamento — os timeouts precisam ser maiores. Em vez de aumentar o proxy_read_timeout global, é mais limpo ter um location dedicado:
location /exportar/ {
proxy_pass http://zig_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_request_buffering off;
}
proxy_request_buffering off é importante para uploads grandes: sem ele, o Nginx bufferiza o corpo inteiro no disco antes de repassar ao upstream, o que dobra o tempo de upload e lota o disco temporário. Para streaming de corpo, desligar o buffering é essencial — e combina com o padrão discutido em validação de uploads com streaming em Zig.
Backpressure é o outro lado da mesma moeda. Quando o serviço Zig satura e o socket enche, o Nginx precisa ver o erro e parar de mandar tráfego, em vez de enfileirar requisições. A combinação max_fails=3 fail_timeout=10s no upstream com um proxy_next_upstream error timeout http_502 http_503 http_504; no location faz exatamente isso: diante de um erro, o proxy tenta a próxima réplica; sem réplica disponível, devolve 502/503 ao cliente em vez de espernear. Para detalhes de como o serviço deve reagir quando está sob pressão, rate limiting com token bucket e circuit breaker, timeout e retry cobrem o desenho interno.
WebSockets por trás do proxy
WebSockets usam o mecanismo HTTP Upgrade. Por padrão, o Nginx não repassa esse header, e a conexão morre com erro 400 ou fecha logo depois do handshake. O bloco correto para WebSockets é:
location /ws/ {
proxy_pass http://zig_backend;
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_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Dois pontos críticos. Primeiro, Connection "upgrade" (com aspas, literal) é o que ativa o modo tunnel do proxy — sem ele, o Nginx trata a conexão como HTTP comum e corta após a primeira resposta. Segundo, proxy_read_timeout 3600s evita que o Nginx feche conexões WebSocket ociosas após 60 segundos (o default). Para clientes que mantêm conexões longas com pouco tráfego (painéis de monitoramento, chat), esse timeout precisa cobrir o intervalo entre heartbeats.
O serviço Zig, no lado do socket, precisa responder ao ping do cliente com pong e fechar conexões que não falam há mais que o heartbeat combinado. Caso contrário, o proxy mantém sockets zumbis abertos até o limite de file descriptors do processo. Os detalhes de implementação do servidor WebSocket em Zig — handshake, frames texto, ping/pong, limites de payload — estão em Zig e WebSockets: servidor real-time e em cliente WebSocket em Zig.
Unix socket em vez de porta TCP
Para deploys de uma única máquina, um socket de domínio Unix entre o Nginx e o serviço Zig costuma ser melhor que uma porta TCP: não passa pela pilha de rede, não consume porta, e o Nginx consegue reaproveitar conexões mais rápido. O serviço Zig abre o socket, e o Nginx aponta para o arquivo:
upstream zig_backend {
server unix:/run/zig/app.sock max_fails=3 fail_timeout=10s;
keepalive 32;
}
Os cuidados são dois. O socket precisa pertencer ao usuário do Nginx (ou a um grupo compartilhado) e ter permissão de leitura/escrita — chown :www-data /run/zig/app.sock && chmod 660 /run/zig/app.sock. E o diretório /run/zig/ precisa existir após reboot, o que se resolve com um RuntimeDirectory=zig na unit do systemd, conforme detalhado em Zig com systemd: serviço Linux em produção.
Headers de segurança e HSTS
Como o Nginx é a borda, é nele que entram os headers de segurança que valem para todas as rotas. O serviço Zig não precisa repeti-los:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
HSTS (Strict-Transport-Security) só faz sentido depois que o TLS está rodando estável e todos os subdomínios relevantes também servem HTTPS — caso contrário, o header trava visitantes em domínios ainda não migrados. O sufixo always garante que o header seja enviado mesmo em respostas 4xx/5xx, sem o qual um atacante poderia forjar uma página de erro sem proteção. Os fundamentos de TLS — geração de chaves, renovação, mTLS interno entre serviços — estão cobertos em TLS, HTTPS e mTLS em Zig.
Troubleshooting: 502, 504 e conexões recusadas
Quase todo incidente “meu serviço Zig caiu atrás do Nginx” se resolve olhando o código de erro do proxy.
502 Bad Gateway significa que o Nginx não conseguiu falar com o upstream. Causas típicas: o serviço Zig não está rodando (systemctl status zig-app), ele está ouvindo em 127.0.0.1 mas o Nginx manda para 0.0.0.0, ou o socket unix tem permissão errada. O diagnóstico direto é curl -v http://127.0.0.1:8080/ a partir da própria máquina: se esse curl falhar, o problema está no serviço, não no proxy.
504 Gateway Timeout significa que o Nginx conectou no upstream, mas o serviço não respondeu dentro do proxy_read_timeout. Causas: o serviço está travado em I/O (banco lento, lock disputado), a rota demora mais que o timeout configurado, ou o serviço fez o trabalho mas travou no meio do write da resposta. O diagnóstico é olhar o log do serviço Zig naquele momento e comparar com o proxy_read_timeout do location. Se o serviço precisa de mais tempo, o caminho é subir o timeout apenas para aquela rota, não globalmente.
Conexões recusadas intermitentes em deploy com várias réplicas geralmente indicam que uma réplica reiniciou e o healthcheck do Nginx não percebeu a tempo. A combinação max_fails + fail_timeout precisa casar com o intervalo de restart esperado do serviço. Se o serviço reinicia rápido (menos de fail_timeout), o Nginx ainda manda tráfego para ele durante a janela morta e devolve 502 ao cliente. Uma política de healthcheck ativa (módulo healthcheck ou um location /healthz interno) reduz essa janela.
HTTP/2 quebrado após upgrade do Nginx costuma ser direto: a diretiva listen 443 ssl http2; em versões antigas do Nginx virou http2 on; separado em versões novas. A sintaxe mista compila mas ignora o HTTP/2 silenciosamente. O diagnóstico é curl -v --http2 https://api.exemplo.com.br/ e olhar a linha de negotiation.
Checklist de produção
Antes de declarar o deploy pronto:
- O serviço Zig escuta apenas em
127.0.0.1ou num unix socket, nunca em0.0.0.0:porta. - O upstream tem
max_failsefail_timeoutdefinidos, mesmo com uma única réplica. - Os timeouts do proxy são coerentes com os do serviço e do cliente.
- Os headers
X-Forwarded-*estão sendo repassados e o serviço Zig lêX-Forwarded-Protopara decidir esquema. - HSTS só está ligado depois que todo o domínio serve HTTPS de forma estável.
- WebSockets têm um
locationdedicado comConnection "upgrade"eproxy_read_timeoutalto. - Existe um
location /healthzque o Nginx (ou o orquestrador) usa para saber que a réplica está viva. - Logs de acesso e erro do Nginx estão sendo coletados, com
log_formatincluindo$upstream_response_timee$upstream_addr. - Há um teste de failover manual: matar uma réplica e confirmar que o cliente vê erro zero (ou um 502 breve seguido de recuperação).
O serviço Zig por trás de Nginx é uma combinação comum, estável e barata. O segredo não é nenhuma configuração exótica: é deixar o contrato entre proxy e upstream explícito, alinhar os timeouts, repassar os headers certos e revisar o checklist antes de abrir o tráfego. Feito isso, a operação fica previsível — e quando algo quebra, o diagnóstico sai em minutos a partir dos códigos de erro do proxy, sem adivinhação.