---
title: "Zig por trás de Nginx: Proxy Reverso, TLS, Load Balancing e WebSockets em Produção"
url: "https://ziglang.com.br/artigos/zig-nginx-proxy-reverso-load-balancing/"
markdown_url: "https://ziglang.com.br/artigos/zig-nginx-proxy-reverso-load-balancing.MD"
description: "Como colocar um serviço Zig em produção atrás de Nginx: proxy reverso, terminação TLS, load balancing entre réplicas, WebSockets, timeouts, headers de segurança, backpressure e troubleshooting de conexões recusadas e 502/504."
date: "2026-06-21"
author: ""
---

# Zig por trás de Nginx: Proxy Reverso, TLS, Load Balancing e WebSockets em Produção

Como colocar um serviço Zig em produção atrás de Nginx: proxy reverso, terminação TLS, load balancing entre réplicas, WebSockets, timeouts, headers de segurança, backpressure e troubleshooting de conexões recusadas e 502/504.


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](/tutoriais/zig-http-server/), [servidor HTTP da stdlib](/stdlib/std-http-server/), [Zig com systemd: serviço Linux em produção](/artigos/zig-systemd-servico-linux-producao/), [TLS, HTTPS e mTLS em Zig](/artigos/zig-tls-https-certificados-mtls/), [rate limiting com token bucket](/artigos/zig-rate-limiting-token-bucket/) e [Server-Sent Events em Zig](/artigos/zig-server-sent-events-sse/). 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.1` ou 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:

```nginx
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:

```nginx
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`:

```nginx
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:

```nginx
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](/artigos/zig-upload-arquivos-validacao-streaming/).

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](/artigos/zig-rate-limiting-token-bucket/) e [circuit breaker, timeout e retry](/artigos/zig-circuit-breaker-timeout-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 é:

```nginx
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](/tutoriais/zig-websockets/) e em [cliente WebSocket em Zig](/receitas/como-criar-um-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:

```nginx
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](/artigos/zig-systemd-servico-linux-producao/).

## 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:

```nginx
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](/artigos/zig-tls-https-certificados-mtls/).

## 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.1` ou num unix socket, nunca em `0.0.0.0:porta`.
- O upstream tem `max_fails` e `fail_timeout` definidos, 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-Proto` para decidir esquema.
- HSTS só está ligado depois que todo o domínio serve HTTPS de forma estável.
- WebSockets têm um `location` dedicado com `Connection "upgrade"` e `proxy_read_timeout` alto.
- Existe um `location /healthz` que 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_format` incluindo `$upstream_response_time` e `$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.
