Zig com systemd: Serviço Linux em Produção

Rodar um binário Zig em produção não precisa começar por Kubernetes. Para muitos projetos brasileiros, a primeira operação real acontece em uma VPS Linux, em um servidor de laboratório, em uma máquina industrial, em um host de borda ou em uma instância pequena atrás de Caddy, Nginx ou Cloudflare Tunnel. Nesse cenário, systemd continua sendo uma escolha direta: inicia o processo no boot, reinicia em falhas, captura logs, injeta variáveis de ambiente e permite aplicar limites de segurança sem transformar o deploy em uma plataforma inteira.

Zig combina bem com esse modelo porque gera um executável nativo, sem runtime externo e sem árvore grande de dependências. O risco é confundir essa simplicidade com ausência de operação. Um serviço Zig ainda precisa de usuário dedicado, diretórios previsíveis, configuração clara, logs legíveis, resposta correta a SIGTERM, health check, política de restart e um caminho de rollback. O binário pode ser pequeno; o contrato operacional não deve ser improvisado.

Este guia mostra uma abordagem prática para publicar uma aplicação Zig como serviço systemd no Linux. Ele complementa os textos sobre servidor HTTP em produção, configuração segura com variáveis de ambiente, observabilidade em Zig, Docker e containers e ferramentas internas para DevOps. Se o seu time também mantém serviços em Go, vale comparar o contrato operacional com referências de ecossistema como golang.com.br, porque o desenho de logs, sinais e deploy costuma ser parecido mesmo quando a linguagem muda.

Quando systemd é suficiente

Use systemd quando você precisa operar um ou poucos processos em um host Linux conhecido. É um bom encaixe para APIs pequenas, workers internos, agentes locais, coletores, servidores TCP, ferramentas de automação que ficam residentes e serviços que não exigem escalonamento horizontal imediato.

Ele deixa de ser suficiente quando a aplicação precisa de scheduling complexo, autoscaling, isolamento multi-tenant, service discovery dinâmico, rollout progressivo entre dezenas de réplicas ou gestão declarativa de muitos hosts. Nesses casos, containers orquestrados podem fazer sentido. Mas não pule para Kubernetes só porque “produção” parece sinônimo de cluster. Uma unit bem escrita, monitoramento simples e rollback documentado resolvem muita coisa.

Estrutura de arquivos no servidor

Evite rodar o binário a partir do diretório do usuário que fez o deploy. Escolha uma estrutura explícita:

/opt/minha-api/
  bin/minha-api
  releases/2026-05-31-0410/minha-api
  shared/
    data/
    cache/
    tmp/
/etc/minha-api/
  minha-api.env
/var/log/minha-api/       # opcional; journalctl pode bastar

Um padrão simples é manter /opt/minha-api/bin/minha-api como symlink para a versão atual em releases/. O rollback vira trocar o symlink e reiniciar o serviço. Para aplicações pequenas, copiar o binário direto para /opt/minha-api/bin/ também funciona, desde que você saiba voltar para a versão anterior.

Crie um usuário sem login para executar o processo:

sudo useradd --system --home /opt/minha-api --shell /usr/sbin/nologin minha-api
sudo mkdir -p /opt/minha-api/bin /opt/minha-api/shared/data /etc/minha-api
sudo chown -R minha-api:minha-api /opt/minha-api
sudo chmod 750 /opt/minha-api

Não rode o serviço como root só porque ele precisa abrir porta baixa ou acessar um arquivo. Prefira usar proxy reverso na porta 80/443, ajustar permissões específicas ou conceder capacidades mínimas quando for inevitável.

Compile um binário previsível

No ambiente de build, gere um executável otimizado e copie somente o artefato necessário:

zig build -Doptimize=ReleaseSafe
install -m 0755 zig-out/bin/minha-api /tmp/minha-api

ReleaseSafe mantém checks de segurança úteis, enquanto reduz bastante o custo em relação ao modo debug. Para um serviço exposto à rede, essa costuma ser uma primeira escolha melhor que ReleaseFast, a menos que você tenha benchmark e perfil de risco bem definidos. Se o binário depende de arquivos externos, templates ou migrações, trate esses itens como parte do release, não como detalhe manual.

Registre a versão no próprio binário quando possível. Uma rota /version, um comando --version ou uma linha de boot como version=2026-05-31 git=abc123 ajuda muito na hora de conferir se o host está rodando o que você acabou de publicar.

Unit file mínimo

Um arquivo /etc/systemd/system/minha-api.service pode começar assim:

[Unit]
Description=Minha API Zig
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=minha-api
Group=minha-api
WorkingDirectory=/opt/minha-api
EnvironmentFile=/etc/minha-api/minha-api.env
ExecStart=/opt/minha-api/bin/minha-api
Restart=on-failure
RestartSec=3s
TimeoutStopSec=20s
KillSignal=SIGTERM

[Install]
WantedBy=multi-user.target

Esse unit file já dá um contrato melhor que iniciar o processo com nohup ou screen. Restart=on-failure cobre crash e exit code inesperado. TimeoutStopSec dá tempo para o serviço encerrar conexões e liberar recursos antes do systemd partir para kill mais forte. EnvironmentFile separa configuração do binário.

O arquivo de ambiente pode ser assim:

PORT=8080
LOG_LEVEL=info
DATABASE_PATH=/opt/minha-api/shared/data/app.sqlite
PUBLIC_BASE_URL=https://minha-api.exemplo.com.br

Não coloque segredos nesse arquivo se ele for gerenciado por um processo frouxo de backup, chat ou wiki. Quando houver tokens, limite permissão com chmod 640, dono root:minha-api, e documente onde a fonte do segredo vive. O artigo de configuração segura em Zig cobre essa fronteira com mais detalhes.

Código Zig preparado para systemd

O serviço precisa escrever logs em stdout ou stderr, carregar configuração no boot e responder a SIGTERM. Para um servidor HTTP, a ideia é não aceitar trabalho novo depois do sinal e dar tempo para requisições em andamento terminarem.

Um esqueleto reduzido:

const std = @import("std");
const posix = std.posix;

var stopping = std.atomic.Value(bool).init(false);

fn handleTerm(_: c_int) callconv(.C) void {
    stopping.store(true, .seq_cst);
}

pub fn main() !void {
    const action = posix.Sigaction{
        .handler = .{ .handler = handleTerm },
        .mask = posix.empty_sigset,
        .flags = 0,
    };
    try posix.sigaction(posix.SIG.TERM, &action, null);
    try posix.sigaction(posix.SIG.INT, &action, null);

    std.log.info("service_start port={d}", .{8080});

    while (!stopping.load(.seq_cst)) {
        // aceite conexões, processe fila ou execute o loop principal
        std.time.sleep(100 * std.time.ns_per_ms);
    }

    std.log.info("service_stop reason=signal", .{});
}

Esse exemplo não substitui uma arquitetura completa, mas fixa o ponto principal: sinal não é exceção misteriosa. É parte normal da operação. O conteúdo de signals em Zig aprofunda o assunto, e o guia de filas e workers mostra por que encerramento limpo importa para tarefas em background.

Logs com journalctl

Se o processo escreve em stdout e stderr, o systemd captura automaticamente:

sudo journalctl -u minha-api.service -f
sudo journalctl -u minha-api.service --since "1 hour ago"
sudo systemctl status minha-api.service

Prefira logs de uma linha por evento, com campos estáveis. Exemplo:

level=info event=request_done method=GET path=/healthz status=200 duration_ms=3
level=error event=db_open_failed path=/opt/minha-api/shared/data/app.sqlite error=AccessDenied

Não dependa de cor, barra de progresso ou mensagens interativas. Serviço Linux não é sessão de terminal. Logs precisam sobreviver a cópia, agregação e busca por texto.

Hardening sem exagero

Depois que o serviço sobe, aplique algumas proteções no unit file:

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/minha-api/shared
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true

ProtectSystem=strict torna o sistema de arquivos majoritariamente somente leitura para o processo. ReadWritePaths libera apenas o diretório onde a aplicação realmente precisa gravar. NoNewPrivileges reduz risco de escalada. PrivateTmp isola /tmp. Nem toda aplicação aceita todos esses limites de primeira, então aplique incrementalmente e valide.

Se o serviço precisa bindar em porta 80 diretamente, você pode usar AmbientCapabilities=CAP_NET_BIND_SERVICE, mas geralmente é melhor deixar Caddy ou Nginx na frente e manter a aplicação Zig em 127.0.0.1:8080.

Deploy e rollback

Um fluxo manual seguro:

sudo install -m 0755 minha-api /opt/minha-api/releases/2026-05-31-0410/minha-api
sudo ln -sfn /opt/minha-api/releases/2026-05-31-0410/minha-api /opt/minha-api/bin/minha-api
sudo systemctl daemon-reload
sudo systemctl restart minha-api.service
sudo systemctl status minha-api.service
curl -fsS http://127.0.0.1:8080/healthz

Rollback:

sudo ln -sfn /opt/minha-api/releases/2026-05-30-2215/minha-api /opt/minha-api/bin/minha-api
sudo systemctl restart minha-api.service
curl -fsS http://127.0.0.1:8080/healthz

O importante é validar depois do restart. systemctl restart retornar zero não prova que a aplicação está saudável; prova apenas que o comando foi aceito. A rota de health check, os logs e uma requisição real precisam confirmar o deploy.

Checklist final

Antes de chamar o serviço de produção, confira:

  • o binário foi compilado com versão de Zig conhecida;
  • o serviço roda com usuário dedicado, não como root;
  • configuração obrigatória falha no boot com mensagem clara;
  • segredos não aparecem em logs, argumentos de processo ou Git;
  • SIGTERM encerra o loop principal com limpeza mínima;
  • Restart=on-failure está configurado com atraso pequeno;
  • logs aparecem em journalctl -u nome.service;
  • diretórios de escrita são explícitos;
  • hardening foi aplicado e testado;
  • existe rollback simples para a versão anterior;
  • uma rota ou comando de health/version confirma o release ativo.

A vantagem de Zig nesse ambiente é tornar o deploy pequeno e audível. Você sabe qual binário está rodando, qual usuário executa, quais arquivos ele pode tocar e como ele termina. systemd não é glamour de plataforma, mas é uma base madura para serviços Linux simples. Para muitos produtos, essa clareza vale mais que uma arquitetura distribuída prematura.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.