Do nohup ao systemd: escrevendo uma unit para a sua aplicação
A aplicação sobe com nohup, morre quando o terminal fecha e ninguém sabe se está no ar. Uma unit do systemd tem quinze linhas e resolve reinício, log, ordem de boot e limites.
Equipe EasyOps Cloud · · 7 min de leitura
A aplicação está no ar porque alguém rodou nohup node server.js & numa sessão
SSH há três meses. Funciona. Até a máquina reiniciar, ou o processo morrer, ou
alguém precisar descobrir se ele está mesmo rodando.
O systemd resolve isso com um arquivo de quinze linhas, e resolve várias coisas
de uma vez: sobe no boot, reinicia ao morrer, registra log estruturado, declara
dependências e aplica limites de recurso. Não há motivo para manter nohup, screen
ou gerenciador de processo específico de linguagem em servidor Linux.
A unit mínima que já vale
# /etc/systemd/system/meuapp.service
[Unit]
Description=API do MeuApp
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=meuapp
Group=meuapp
WorkingDirectory=/var/www/meuapp
ExecStart=/usr/bin/node /var/www/meuapp/server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now meuapp
systemctl status meuappTrês detalhes valem atenção já nesse mínimo.
O ExecStart exige caminho absoluto. O systemd não usa o seu PATH, pelo mesmo
motivo que o cron não usa — é um ambiente próprio e enxuto. Se o binário vem de
gerenciador de versão como nvm, aponte o caminho real, que which node revela.
O User evita rodar como root. Serviço de aplicação não precisa de root, e rodar
como root transforma qualquer falha da aplicação em comprometimento da máquina.
O enable --now faz duas coisas: habilita no boot e inicia agora. Habilitar sem
iniciar é a causa clássica do "configurei e não subiu".
Escolher o Type certo
Errar aqui produz um sintoma específico e confuso: o systemctl start trava, ou
retorna sucesso com o serviço fora do ar.
| Type | Quando usar |
|---|---|
| simple | O processo fica em primeiro plano. Padrão da maioria |
| exec | Como simple, mas espera o binário executar de fato |
| forking | O processo se duplica e o pai sai — daemon tradicional |
| oneshot | Roda, termina e pronto. Para tarefa, não serviço |
| notify | A aplicação avisa o systemd quando está pronta |
A regra prática: se a sua aplicação não vira daemon sozinha — e a maioria das
aplicações modernas não vira —, use simple. Se ela tem opção de rodar em
primeiro plano, use essa opção; é sempre preferível a forking.
Se você usa forking sem PIDFile, o systemd perde a referência do processo e
passa a achar que o serviço caiu.
Para o serviço só ser considerado ativo quando estiver realmente pronto a
receber tráfego, notify é o certo — mas exige suporte na aplicação. Uma
alternativa prática é um teste de disponibilidade em ExecStartPost.
Reinício: as opções e a armadilha
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5Restart=on-failure reinicia quando o processo termina com erro, e não quando ele
sai com sucesso. É o comportamento desejado quase sempre. Restart=always
reinicia até quando você para o serviço de propósito por outro caminho — use com
consciência.
As duas últimas linhas são a proteção contra laço de reinício. Sem elas, uma aplicação que falha na inicialização por configuração errada entra em ciclo: reinicia, falha, reinicia, para sempre, enchendo o log e consumindo CPU.
Com elas, após 5 tentativas em 300 segundos o systemd desiste e deixa o serviço em falha — que é o estado correto, porque assim o problema fica visível em vez de mascarado.
Se cair nesse estado depois de corrigir, é preciso limpar o contador:
sudo systemctl reset-failed meuapp
sudo systemctl start meuappVariáveis de ambiente e segredos
Não coloque credencial dentro da unit — o arquivo é legível por qualquer usuário do sistema.
EnvironmentFile=/etc/meuapp/env
Environment=NODE_ENV=productionsudo install -o root -g meuapp -m 640 /dev/null /etc/meuapp/envO arquivo aceita CHAVE=valor, um por linha, sem export e sem aspas — o systemd
não interpreta sintaxe de shell, e aspas viram parte do valor.
Se o arquivo não existir, o serviço nem sobe. Para torná-lo opcional, prefixe
com hífen: EnvironmentFile=-/etc/meuapp/env.
Para conferir o ambiente que o serviço realmente recebeu:
sudo systemctl show meuapp -p Environment
sudo cat /proc/$(systemctl show -p MainPID --value meuapp)/environ | tr '\0' '\n'Ordem e dependências
After= define ordem; Requires= define dependência real. São coisas diferentes e
confundi-las causa falha intermitente no boot.
[Unit]
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.serviceAfter sozinho apenas ordena: se o Postgres não subir, a sua aplicação sobe do
mesmo jeito e falha ao conectar. Requires faz o systemd parar a sua aplicação se
a dependência falhar.
Um detalhe frequente: network.target significa que a rede foi configurada, não
que ela está funcionando. Para aplicação que precisa de conectividade real na
partida, o correto é network-online.target com o Wants correspondente.
Vale evitar Requires para banco em outra máquina — o systemd local não sabe nada
sobre ele. Nesse caso, a resiliência tem que estar na aplicação: tentar
reconectar em vez de morrer.
Limites e isolamento de graça
Diretivas que custam uma linha e reduzem bastante o estrago potencial:
# Recursos
MemoryMax=2G
CPUQuota=150%
LimitNOFILE=65535
# Isolamento
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/meuapp/storageProtectSystem=strict monta todo o sistema de arquivos como somente leitura para
esse serviço, exceto o que você liberar em ReadWritePaths. Uma falha na
aplicação deixa de conseguir alterar binário do sistema ou o próprio código.
PrivateTmp=true dá ao serviço um /tmp isolado, o que elimina uma classe inteira
de ataque por arquivo temporário previsível.
O MemoryMax é a proteção descrita no artigo sobre o OOM killer: um vazamento
derruba o próprio serviço em vez de derrubar o banco de dados que estava usando
mais memória.
Depois de aplicar, vale conferir o quanto o serviço ficou exposto:
systemd-analyze security meuappEle lista cada diretiva de proteção e aponta o que falta. Não persiga a nota perfeita, mas as três ou quatro primeiras sugestões costumam ser fáceis e valiosas.
Log, que passa a ser de graça
Com a unit, stdout e stderr vão para o journal automaticamente. Some o
redirecionamento manual para arquivo:
journalctl -u meuapp -f # acompanhar ao vivo
journalctl -u meuapp -n 100 # últimas 100 linhas
journalctl -u meuapp --since '1 hour ago' -p err
journalctl -u meuapp -o json-pretty # com todos os camposVale garantir que o journal tenha retenção definida, senão ele cresce até incomodar — o artigo sobre disco cheio traz os parâmetros.
Quando não é serviço, é tarefa
Nem tudo que precisa rodar é um processo permanente. Para o que executa e
termina — backup, importação, limpeza —, a unit é do tipo oneshot, acompanhada
de um timer.
# /etc/systemd/system/importacao.service
[Unit]
Description=Importação diária de pedidos
[Service]
Type=oneshot
User=meuapp
EnvironmentFile=/etc/meuapp/env
ExecStart=/var/www/meuapp/bin/importar# /etc/systemd/system/importacao.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetsudo systemctl enable --now importacao.timer
systemctl list-timers importacao.timerTrês diretivas justificam a troca em relação ao cron. Persistent=true executa a
tarefa perdida quando a máquina estava desligada no horário — o cron
simplesmente pula. RandomizedDelaySec espalha a partida, evitando que dez
tarefas marcadas para 3h disputem disco no mesmo instante. E o log vai para o
journal, com journalctl -u importacao, sem redirecionamento manual.
Some a isso a proteção contra sobreposição: se a execução anterior ainda estiver rodando, o systemd não inicia outra. É a diferença entre um processamento lento e duas cópias concorrentes corrompendo o mesmo dado — problema que o cron não resolve sozinho e que costuma aparecer no pior momento, quando o volume cresce.
Vale dizer que o cron continua adequado para tarefa simples e sem estado. A troca compensa quando o horário importa, quando a duplicidade é perigosa ou quando o log precisa estar em algum lugar. O artigo sobre por que o cron não roda cobre os casos em que manter o cron faz sentido.
Migrando do que já existe
Se hoje a aplicação roda com um gerenciador de processo próprio da linguagem, o caminho é direto: uma unit por processo, ou uma unit que chama o gerenciador em modo primeiro plano. A segunda opção mantém a compatibilidade, mas empilha dois supervisores — e quando algo der errado, você vai depurar os dois.
Depois de subir, teste o que realmente importa antes de considerar pronto:
sudo systemctl restart meuapp && systemctl is-active meuapp
sudo kill -9 $(systemctl show -p MainPID --value meuapp)
sleep 8 && systemctl status meuapp # tem que ter voltado sozinho
sudo reboot # e voltar depois do bootEsses três testes cobrem o que o nohup nunca ofereceu. Se algo falhar, o diagnóstico
de serviço que não sobe cobre a leitura de exit code e as causas mais comuns.
Vale versionar a unit junto com o código da aplicação, em vez de deixá-la existindo apenas no servidor. Ela descreve como o serviço roda — usuário, ambiente, dependências, limites — e essa informação é tão parte do sistema quanto o código. Um servidor novo provisionado a partir do repositório sobe idêntico; um servidor cuja unit foi escrita à mão em algum momento esquecido não sobe nunca igual.