Criar um serviço systemd próprio e executá-lo automaticamente no arranque
O ficheiro de unidade linha a linha: Type, User, WorkingDirectory, Restart e network-online.target. Com as mensagens de erro na forma literal e a prova de que o serviço sobrevive mesmo a um reinício.
Um programa que não regressa sozinho depois de um reinício não é um serviço num servidor, é um risco. O nohup, o screen e o tmux mantêm um processo vivo enquanto nada acontecer. Este artigo escreve um ficheiro de unidade próprio linha a linha, mostra as mensagens de erro na sua forma literal e explica como sair do carrossel de arranques quando o serviço fica lá preso.
Todos os comandos correm como root. Testado em Debian 13 (systemd 257), Debian 12 (252), Ubuntu 24.04 LTS (255) e Ubuntu 22.04 LTS (249). Sempre que os quatro sistemas divergem, fica assinalado.
O que um serviço systemd faz e o que o nohup e o screen não fazem
A diferença não está na comodidade, está na responsabilidade. O systemd arranca o processo no boot por uma ordem definida, coloca-o numa cgroup própria, recolhe o stdout e o stderr no journal, reinicia-o depois de uma falha e termina-o de forma limpa no shutdown com SIGTERM. A cgroup é aquilo cuja falta mais dói: um script iniciado com nohup deixa para trás processos filhos órfãos, uma unidade limpa o seu grupo por completo.
Preparação: programa, utilizador e diretório
Antes de a unidade existir, tem de funcionar aquilo que ela vai arrancar. Como exemplo serve um script que escreve no stdout. É precisamente esse o ponto: um serviço sob o systemd não escreve os seus próprios registos para um ficheiro, escreve no stdout, e o systemd coloca tudo no journal.
mkdir -p /opt/kh-demo
cat > /opt/kh-demo/run.sh <<'EOF'
#!/bin/bash
set -euo pipefail
while true; do
echo "kh-demo vivo, $(date --iso-8601=seconds), PWD=$PWD, GREETING=${GREETING:-não definido}"
sleep 10
done
EOF
chmod +x /opt/kh-demo/run.sh
A seguir, um utilizador de sistema próprio: --system atribui um UID abaixo de 1000, /usr/sbin/nologin impede sessões interativas.
useradd --system --no-create-home --home-dir /opt/kh-demo --shell /usr/sbin/nologin khdemo
id khdemo
chown -R root:khdemo /opt/kh-demo
chmod 750 /opt/kh-demo
O teste decisivo antes do ficheiro de unidade: o programa corre com este utilizador? Quem salta este passo acaba mais tarde a depurar o systemd, quando o problema está no programa.
timeout 3 runuser -u khdemo -- /opt/kh-demo/run.sh; echo "Código de saída $?"
Código de saída 124 é aqui o resultado desejado: o timeout interrompeu um programa que estava a correr. Qualquer outro valor significa que o script morreu por si próprio, e nesse caso a culpa não é do systemd.
O ficheiro de unidade linha a linha
As unidades próprias pertencem a /etc/systemd/system/ e não a /lib/systemd/system/ ou /usr/lib/systemd/system/: estes dois diretórios são do gestor de pacotes e ficam sobrescritos no próximo apt upgrade.
cat > /etc/systemd/system/kh-demo.service <<'EOF'
[Unit]
Description=KernelHost Demo Worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=khdemo
Group=khdemo
WorkingDirectory=/opt/kh-demo
EnvironmentFile=-/etc/kh-demo.env
ExecStart=/opt/kh-demo/run.sh
Restart=on-failure
RestartSec=5s
TimeoutStopSec=20s
SyslogIdentifier=kh-demo
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target
EOF
Secção [Unit]
Description= é o texto que aparece no systemctl status. Não tem efeito técnico, mas é o que decide se às três da manhã percebe o que é que falhou. Uma frase, não uma palavra.
After= define apenas a ordem, não a dependência. After=network-online.target significa: se este target for arrancado neste boot de qualquer forma, espera por ele. Não significa que ele seja arrancado.
Wants= é que o pede de facto, e ainda assim como dependência fraca. Se o target falhar, o serviço arranca à mesma. A contrapartida Requires= arrastaria o serviço para o erro. Para a maioria das aplicações, Wants= é a escolha certa, porque um serviço que nem chega a arrancar por causa de um timeout de rede é pior do que um serviço que corre por instantes no vazio e volta graças ao Restart=.
Secção [Service]
User= e Group= definem a identidade do processo. Sem estas linhas, o serviço corre como root. Em Debian e Ubuntu, o useradd cria por omissão um grupo com o mesmo nome, por isso Group=khdemo encaixa.
WorkingDirectory= é o diretório de trabalho do processo. Sem esta indicação, o serviço arranca em /. Qualquer programa que procure ficheiros de configuração, templates ou plugins de forma relativa ao diretório atual afunda-se logo aí. Se o diretório indicado não existir, o arranque termina com 200/CHDIR.
EnvironmentFile= carrega variáveis a partir de um ficheiro. O sinal de menos à frente do caminho torna o ficheiro opcional. Sem esse menos, o arranque falha quando o ficheiro não existe. O conteúdo são simples linhas KEY=VALUE, não é shell. Um export à frente está errado, e PORT=$BASE_PORT não é resolvido.
ExecStart= precisa de um caminho absoluto para o ficheiro executável. É nesta regra que a maioria tropeça, por isso mais abaixo tem uma secção só para ela.
TimeoutStopSec= limita quanto tempo o systemd espera depois do SIGTERM antes de chegar o SIGKILL. Por omissão são 90 segundos.
SyslogIdentifier= define o nome com que as linhas aparecem no journal. Sem esta linha, o remetente chama-se run.sh, o que é inútil na hora de filtrar.
NoNewPrivileges=true proíbe o processo e todos os seus filhos de ganharem privilégios através de binários setuid. PrivateTmp=true dá ao serviço um /tmp próprio. ProtectSystem=full monta /usr, /boot e /etc apenas em leitura. O nível strict vai mais longe e passa a exigir StateDirectory= ou ReadWritePaths= para tudo o que precise de escrita.
Secção [Install]
WantedBy=multi-user.target responde à pergunta de quando é que o serviço deve arrancar automaticamente. multi-user.target é o funcionamento multiutilizador normal sem interface gráfica, ou seja, aquilo que um servidor atinge. Só o systemctl enable avalia esta secção e cria o symlink. Se faltar o [Install], o enable aborta com The unit files have no installation config (WantedBy=, RequiredBy=, Also=, Alias= settings in the [Install] section, and DefaultInstance= for template units). O serviço ainda se deixa arrancar à mão, mas nunca mais volta depois de um reinício.
Type=simple, Type=exec e Type=forking
O Type= responde exatamente a uma pergunta: como é que o systemd reconhece que o serviço arrancou?
Com Type=simple, o serviço conta como arrancado assim que o processo é criado, ou seja, depois do fork() e ainda antes de o programa ser executado. É o valor por omissão e está correto para quase todos os programas modernos que ficam em primeiro plano.
Com Type=exec, o systemd espera ainda que o execve() tenha sido bem sucedido. A diferença prática é considerável: com Type=simple, o systemctl start comunica sucesso mesmo que o binário nem sequer exista, e o erro só aparece depois no journal. Com Type=exec, o arranque falha de imediato exatamente nesse caso. Disponível desde o systemd 240 e, portanto, nas quatro distribuições.
Com Type=forking, o systemd parte do princípio de que o programa se despede sozinho para segundo plano: o processo arrancado termina, um filho continua a correr, e só esse fim conta como sinal de arranque. É o comportamento clássico de um daemon Unix. Quem usa este tipo precisa quase sempre também de PIDFile= com caminho absoluto, caso contrário o systemd fica a adivinhar qual dos processos restantes é o processo principal.
A recomendação é clara: Type=forking só quando não há mesmo maneira de demover o programa disso. Quase todo o software tem uma opção para o efeito, muitas vezes --foreground, -D FOREGROUND, --no-daemon ou daemon off; na configuração. Em primeiro plano e com Type=simple, a unidade fica mais curta, os registos vão parar ao journal e o Restart= funciona de forma fiável.
A combinação errada típica: um programa que se coloca sozinho em segundo plano, debaixo de Type=simple. O systemd vê o processo de arranque terminar, dá o serviço por acabado e mata os filhos com ele. O sintoma é Active: inactive (dead) logo a seguir a um systemctl start que não comunicou qualquer erro. O caso inverso, um programa de primeiro plano debaixo de Type=forking, deixa o systemd à espera de um fim que nunca chega: Job for foo.service failed because a timeout was exceeded, ao fim de 90 segundos.
Ligar corretamente o After=network-online.target
É aqui que quase todos os tutoriais param cedo demais. O network.target significa apenas que a gestão de rede foi arrancada, não que exista um endereço IP configurado. Quem precisa de um endereço alcançável, por exemplo porque o programa se liga a um IP fixo, quer o network-online.target.
Só que este target não é atingido por si só. Pressupõe que esteja ativado um serviço de espera adequado, e esse varia consoante a gestão de rede:
systemctl list-unit-files 'systemd-networkd-wait-online.service' 'NetworkManager-wait-online.service' 'ifupdown-wait-online.service' --no-pager
No Ubuntu Server 22.04 e 24.04, a rede é gerida pelo netplan com systemd-networkd, aí o systemd-networkd-wait-online.service está ativo e o network-online.target tem um significado real. Numa instalação Debian clássica com ifupdown, o ifupdown-wait-online.service existe, mas não está ativado. O target passa então a contar como atingido de imediato, e o tempo de espera com que se está a contar não chega a acontecer.
No Debian 12 e 13 com ifupdown, o serviço de espera ativa-se uma única vez, se for preciso:
systemctl enable ifupdown-wait-online.service
Ativar dois serviços de espera ao mesmo tempo não é boa ideia, porque cada um fica à espera da sua própria gestão de rede e um deles vai forçosamente parar no timeout de 90 segundos. Essa é a causa mais frequente de um servidor que de repente demora mais um minuto e meio a arrancar. Mais robusto do que qualquer ordem de arranque é, de qualquer forma, um programa que aguenta uma ligação em falta no arranque, combinado com Restart=on-failure.
Restart, RestartSec e o travão de arranque
Restart=on-failure reinicia quando o código de saída é diferente de zero, perante um sinal como SIGSEGV e perante um timeout do watchdog, mas não depois de um exit 0 nem depois de um systemctl stop manual. Restart=always reinicia também depois de um fim limpo, e é essa a forma mais rápida de construir um ciclo infinito.
Contra isso, o systemd tem um travão incorporado: por omissão são permitidas cinco tentativas de arranque em dez segundos (StartLimitBurst=5, StartLimitIntervalSec=10s). A partir daí o systemd desiste e comunica:
kh-demo.service: Start request repeated too quickly.
kh-demo.service: Failed with result 'exit-code'.
Failed to start kh-demo.service - KernelHost Demo Worker.
O serviço fica então no estado failed e deixa de reagir mesmo ao systemctl start, até o contador ser reposto:
systemctl reset-failed kh-demo.service
Há dois pormenores que custam tempo com regularidade. Primeiro, StartLimitBurst= e StartLimitIntervalSec= pertencem à secção [Unit], não a [Service]. Na secção errada são ignorados, com aviso no journal, mas sem erro. Segundo, as chamadas manuais a systemctl restart também contam: quem reinicia cinco vezes enquanto depura aciona o travão sozinho.
Regra prática: StartLimitIntervalSec maior do que RestartSec vezes StartLimitBurst.
No Debian 13 e no Ubuntu 24.04 existem ainda RestartSteps= e RestartMaxDelaySec= (a partir do systemd 254) para tempos de espera com crescimento exponencial. No Debian 12 e no Ubuntu 22.04 estas diretivas não existem e são ignoradas.
Ativar, arrancar e provar que funciona
Antes do primeiro arranque compensa a verificação de sintaxe. Encontra erros de escrita nos nomes das diretivas, secções mal escritas e programas em falta, sem arrancar seja o que for:
systemd-analyze verify /etc/systemd/system/kh-demo.service
Nenhuma saída significa: está tudo em ordem. Um erro de escrita como WorkingDirectiry= produz, consoante a versão do systemd, Unknown key name 'WorkingDirectiry' in section 'Service', ignoring ou Unknown key 'WorkingDirectiry' in section [Service], ignoring. É aqui que nascem os erros silenciosos: o systemd ignora chaves desconhecidas, o serviço arranca, mas comporta-se de forma diferente do esperado.
systemctl daemon-reload
O daemon-reload volta a ler os ficheiros de unidade, mas não reinicia nada. Quem se esquece do reload recebe no systemctl status seguinte: Warning: The unit file, source configuration file or drop-ins of kh-demo.service changed on disk. Run 'systemctl daemon-reload' to reload units. Vale a pena decorar a ordem: primeiro daemon-reload, depois restart.
systemctl enable kh-demo.service
systemctl start kh-demo.service
As duas coisas de uma vez fazem-se com systemctl enable --now kh-demo.service.
Agora a parte que a maioria dos tutoriais deixa de fora: a prova de que aquilo funcionou mesmo e não apenas de que não apareceu um erro. Quatro provas independentes.
Primeiro, o symlink existe. Ele é todo o mecanismo por detrás do enable. Se faltar, o serviço não arranca depois do reinício, diga o status o que disser nesse momento.
ls -l /etc/systemd/system/multi-user.target.wants/kh-demo.service
systemctl is-enabled kh-demo.service
Segundo, o estado. O que decide é o que está entre parênteses na linha Active:. active (running) quer dizer que existe um processo a correr. active (exited) quer dizer que o programa terminou e já não está lá ninguém. Para um serviço permanente isso é um erro, mesmo que ao lado esteja verde.
systemctl status kh-demo.service --no-pager
systemctl is-active kh-demo.service
Terceiro, o journal. Não só se chegam linhas, mas se chegam as linhas certas.
journalctl -u kh-demo.service -n 20 --no-pager
Acompanhar em tempo real: journalctl -u kh-demo.service -f. Só o último boot: -b. Intervalo de tempo: --since "-1h". Mais sobre o tema no artigo journalctl: analisar logs sob o systemd.
Quarto, a configuração efetiva. Não o ficheiro, mas aquilo que o systemd fez dele. A diferença conta assim que houver drop-ins pelo meio.
systemctl show kh-demo.service -p ExecStart -p User -p WorkingDirectory -p Restart -p MainPID
systemctl cat kh-demo.service
A prova final continua a ser um reinício a sério. Depois dele, este comando mostra se ficou alguma coisa pelo caminho:
systemctl list-units --type=service --state=failed --no-pager
Os erros mais frequentes na sua forma literal
O systemd comunica os erros de arranque através de códigos de saída próprios acima de 200. O número que aparece no status já diz onde é preciso procurar.
| Código | Significado | Causa |
|---|---|---|
| 200/CHDIR | EXIT_CHDIR | WorkingDirectory= não existe ou o utilizador não consegue entrar nele |
| 203/EXEC | EXIT_EXEC | ExecStart= não encontrado, não executável ou interpretador errado |
| 216/GROUP | EXIT_GROUP | Group= não existe |
| 217/USER | EXIT_USER | User= não existe |
| 219/CGROUP | EXIT_CGROUP | não foi possível criar a cgroup |
| 238/STATE_DIRECTORY | EXIT_STATE_DIRECTORY | StateDirectory= já existe com o proprietário errado |
203/EXEC: o caminho errado em ExecStart
No journal fica uma linha do género kh-demo.service: Failed at step EXEC spawning /opt/kh-demo/run.sh: No such file or directory, seguida de Main process exited, code=exited, status=203/EXEC.
No such file or directory é enganador, porque a mensagem tem três causas. Primeira: o ficheiro não existe mesmo, quase sempre por causa de um erro de escrita ou porque o binário está em /usr/local/bin em vez de /usr/bin. Segunda: falta o bit de execução, e nesse caso o journal comunica Permission denied. Terceira, o caso mais traiçoeiro: o ficheiro é executável, mas a sua linha shebang aponta para o vazio. Um script com #!/usr/bin/python falha exatamente assim no Debian 12 e mais recentes, porque aí só existe /usr/bin/python3. O kernel comunica a falta do interpretador, o systemd passa isso adiante como falta do script.
A verificação demora três segundos:
ls -l /opt/kh-demo/run.sh
head -n 1 /opt/kh-demo/run.sh
Caminhos relativos e PATH
ExecStart=node server.js não funciona, nem sequer com WorkingDirectory= definido. O erro ao carregar a unidade é Neither a valid executable name nor an absolute path. Só a primeira entrada tem de ser absoluta, os argumentos seguintes podem ficar relativos. O correto é ExecStart=/usr/bin/node server.js, sendo que o server.js é resolvido de forma relativa ao WorkingDirectory. O caminho certo é dado pelo command -v:
command -v bash
Atenção aos gestores de versões: com o nvm isso devolve um caminho como /root/.nvm/versions/node/v22.14.0/bin/node, que não existe para o utilizador do serviço. Os runtimes destinados a serviços instalam-se ao nível de todo o sistema, por exemplo através da NodeSource ou da Adoptium Temurin.
Variáveis de ambiente em falta
O clássico: numa sessão interativa o programa corre, como serviço não. A razão é que o systemd não arranca uma shell de login. Nem o /etc/profile nem o ~/.bashrc nem o ~/.profile são lidos, e tudo o que aí esteja definido com export falta no serviço. O PATH de um serviço de sistema é um caminho mínimo fixo, normalmente /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin.
Por isso, as variáveis vão para a unidade ou para um ficheiro próprio:
cat > /etc/kh-demo.env <<'EOF'
GREETING=olá
LANG=pt_PT.UTF-8
EOF
chmod 640 /etc/kh-demo.env
chown root:khdemo /etc/kh-demo.env
systemctl restart kh-demo.service
journalctl -u kh-demo.service -n 5 --no-pager
Se na saída aparecer agora GREETING=olá em vez de GREETING=não definido, o ficheiro chegou ao destino. Também se pode verificar diretamente:
systemctl show kh-demo.service -p Environment -p EnvironmentFiles
Três armadilhas neste tipo de ficheiros: a substituição de comandos com $(...) não acontece, um $ com sentido literal tem de ser escrito como $$, e as aspas acabam dentro do valor, onde não têm nada que fazer.
Unit not found
Failed to start kh-demo.service: Unit kh-demo.service not found. significa quase sempre uma de três coisas: o ficheiro está no diretório errado, tem a extensão errada (.services em vez de .service) ou falta o daemon-reload. Um ls -l /etc/systemd/system/ esclarece os dois primeiros casos.
Diferenças entre Debian 13, Debian 12, Ubuntu 24.04 e 22.04
As bases, ou seja [Unit], [Service], [Install], Type=simple, Restart=, User= e WorkingDirectory=, são idênticas nos quatro sistemas. As diferenças estão nas zonas periféricas.
Diretivas disponíveis. O Ubuntu 22.04 traz o systemd 249, o Debian 12 o systemd 252, o Ubuntu 24.04 o systemd 255 e o Debian 13 o systemd 257. Tudo o que surgiu a partir da versão 253 falta nos dois sistemas mais antigos: Type=notify-reload (253) e ainda RestartSteps=, RestartMaxDelaySec= e RestartMode=direct (254). Não são assinaladas como erro, são simplesmente ignoradas. É assim que nascem unidades que num sistema fazem o pretendido e no outro fazem algo diferente, aparentemente sem motivo.
systemctl --version
Rede. O Ubuntu Server usa netplan com systemd-networkd, o Debian na instalação padrão usa ifupdown. Por isso, no Ubuntu o network-online.target tem significado sem qualquer intervenção e no Debian só depois de ativar o ifupdown-wait-online.service.
Caminhos de interpretadores e runtimes. É aqui que nasce a maior parte dos erros 203/EXEC quando se transporta uma unidade entre sistemas. O Node.js está no Debian 13 na versão 20.19, no Debian 12 na 18.20, no Ubuntu 24.04 na 18.19 e no Ubuntu 22.04 na 12.22. O PHP é 8.4 no Debian 13, 8.2 no Debian 12, 8.3 no Ubuntu 24.04 e 8.1 no Ubuntu 22.04. No Java o salto é o maior de todos: o Debian 13 fornece apenas openjdk-21-jre-headless, o Debian 12 apenas openjdk-17-jre-headless, ao passo que o Ubuntu 24.04 e o 22.04 fornecem 8, 11, 17 e 21. Quem copiar ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/java do Debian 12 para o Debian 13 recebe 203/EXEC de forma garantida.
Bases de dados. O Debian não fornece mysql-server, aí corre sempre MariaDB. Uma unidade com After=mysql.service fica, portanto, à espera no Debian de um serviço que não existe, e arranca sem qualquer atraso, em silêncio e sem aviso. O correto aí é After=mariadb.service.
Alterar, reverter, limpar
As unidades próprias alteram-se diretamente no ficheiro, seguido de daemon-reload. Para unidades que vêm de um pacote usam-se antes drop-ins, para que a próxima atualização não varra a personalização:
systemctl edit kh-demo.service
Isso cria o ficheiro /etc/systemd/system/kh-demo.service.d/override.conf, onde ficam apenas as diretivas alteradas. Caso especial: as diretivas de lista, como ExecStart=, não são substituídas, são acrescentadas. Quem as quiser sobrepor tem de esvaziar a lista primeiro:
ExecStart=
ExecStart=/opt/kh-demo/run.sh --verbose
O systemctl revert kh-demo.service volta a remover todos os drop-ins. Desmontagem completa do exemplo, exatamente por esta ordem:
systemctl disable --now kh-demo.service
rm -f /etc/systemd/system/kh-demo.service
rm -rf /etc/systemd/system/kh-demo.service.d
systemctl daemon-reload
systemctl reset-failed
Atenção ao último passo: systemctl reset-failed sem argumento repõe o estado de erro de todas as unidades, não apenas o do serviço de exemplo. Num sistema onde existam ainda outros serviços no estado failed, desaparecem com isso também as mensagens desses serviços de systemctl list-units --state=failed. Quem só quiser limpar aqui usa a forma dirigida systemctl reset-failed kh-demo.service da secção mais acima.
Quem apagar o ficheiro sem antes chamar o disable deixa para trás um symlink morto em /etc/systemd/system/multi-user.target.wants/, que aparece como aviso em cada daemon-reload. Limpa-se com find /etc/systemd/system -xtype l -delete. Só que esta chamada remove todos os symlinks mortos abaixo de /etc/systemd/system, incluindo restos de outros serviços de que talvez ainda precise. Mais seguro é olhar primeiro sem o -delete, ou usar logo a variante dirigida find /etc/systemd/system -xtype l -name 'kh-demo*' -delete. Opcionalmente, ainda o utilizador e os ficheiros:
userdel khdemo
rm -rf /opt/kh-demo /etc/kh-demo.env
Caminho absoluto no ExecStart, Type adequado, utilizador próprio, WorkingDirectory definido, variáveis através de EnvironmentFile=: quem tiver isto tudo reunido tem um serviço que sobrevive a um reinício e que, em caso de erro, revela o que aconteceu.
Perguntas frequentes
Tenho de executar systemctl daemon-reload depois de cada alteração ao ficheiro de unidade?
O que significa status=203/EXEC?
Porque é que o meu serviço não arranca depois do reinício, apesar de o systemctl start funcionar?
Type=simple ou Type=forking?
Porque é que o meu serviço não encontra as suas variáveis de ambiente?
O serviço deixou de reagir ao systemctl start, o que fazer?
2026 KernelHost GmbH. Todos os direitos reservados. Este guia está protegido por direitos de autor. A sua republicação noutros sites, na íntegra, em parte ou de forma editada, não é permitida sem o nosso consentimento por escrito. Citações com indicação da fonte e ligação são expressamente bem-vindas.

