Guide

systemd: guida completa a unit file, hardening e troubleshooting dei servizi Linux

Dario Fadda Luglio 8, 2026

Perché systemd è ancora il cuore di ogni server Linux moderno

Chiunque amministri sistemi Linux ha digitato decine di volte comandi come systemctl start o systemctl enable senza fermarsi troppo a pensare a cosa succede realmente sotto il cofano. systemd è l’init system presente sulla stragrande maggioranza delle distribuzioni Linux in produzione, e gli unit file sono il meccanismo con cui gli si dice cosa eseguire, quando eseguirlo e come comportarsi in caso di errore.

Conoscere a fondo la sintassi degli unit file e le tecniche di troubleshooting non è un esercizio accademico: è ciò che fa la differenza tra risolvere un servizio bloccato in due minuti o passare mezza giornata a indovinare. Vediamo come scrivere service unit robusti, quali opzioni contano davvero e come diagnosticare i fallimenti più comuni.

Anatomia di un service unit file

Un unit file è un file di configurazione testuale che descrive una risorsa gestita da systemd: servizi (.service), timer (.timer), socket (.socket), mount point (.mount) e altro ancora. In questo articolo ci concentriamo sui service unit, che sono quelli con cui la maggior parte dei sistemisti ha a che fare quotidianamente.

Prima regola pratica: sapere dove vivono i file.

  • /lib/systemd/system/ o /usr/lib/systemd/system/ — unit forniti dai pacchetti di sistema, da non modificare mai direttamente
  • /etc/systemd/system/ — dove lavori tu: unit personalizzati e override
  • /run/systemd/system/ — unit runtime, spariscono al reboot

I file in /etc/systemd/system/ hanno precedenza su quelli in /lib/systemd/system/: è così che funzionano gli override.

Un service unit tipico si compone di tre sezioni: [Unit], [Service] e [Install]. Ecco un esempio minimo ma realistico per un’app Python:

[Unit]
Description=My Python Web App
After=network.target

[Service]
Type=simple
User=webapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python app.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

La sezione [Unit]: ordinamento, non dipendenza

After=network.target indica solo l’ordine di avvio, non una vera dipendenza: il servizio parte dopo che la rete di base è stata configurata, ma non è garantito che un’interfaccia sia effettivamente raggiungibile. Per servizi che devono fare connessioni in uscita all’avvio (database, agenti di sincronizzazione, chiamate verso internet) è preferibile usare network-online.target:

[Unit]
After=network-online.target
Wants=network-online.target

Attenzione: questo funziona solo se sul sistema è abilitato un servizio “wait”, come systemd-networkd-wait-online o NetworkManager-wait-online, cosa non garantita su tutte le distribuzioni. Verificalo con:

systemctl is-enabled NetworkManager-wait-online.service

Vale la pena distinguere bene tra i tre operatori di dipendenza: Wants= è una dipendenza soft (se l’unit richiamata fallisce, il tuo servizio parte comunque), Requires= è una dipendenza hard (se fallisce, fallisce anche il tuo servizio), mentre After=/Before= riguardano solo l’ordine di avvio.

La sezione [Service]: dove si gioca la partita

Il parametro Type= descrive come si comporta il processo all’avvio, e sbagliarlo è una delle cause più comuni di servizi che sembrano non funzionare mai correttamente:

  • Type=simple (default) — systemd considera il servizio avviato non appena parte il processo ExecStart. Va bene per processi in foreground.
  • Type=forking — per demoni “vecchio stile” che fanno fork in background. systemd attende che il processo padre termini; serve quasi sempre PIDFile=.
  • Type=notify — il processo notifica la propria disponibilità via sd_notify(). Più affidabile di simple per applicazioni complesse.
  • Type=oneshot — per script che eseguono e terminano. Aggiungi RemainAfterExit=yes se vuoi che risulti “active” anche dopo l’uscita.
  • Type=exec (systemd 240+) — simile a simple, ma systemd attende l’effettiva execve() del binario prima di considerare il servizio avviato, intercettando i casi in cui ExecStart non riesce nemmeno a partire.

Un dettaglio spesso sottovalutato riguarda i wrapper shell in ExecStart. Se usi:

ExecStart=/bin/bash -c 'echo started >> /var/log/myapp.log && /opt/myapp/start.sh'

ricorda che systemctl stop invia SIGTERM alla shell, non alla tua applicazione, rompendo potenzialmente lo shutdown pulito. Se proprio serve un wrapper, usa exec sul comando finale (exec /opt/myapp/start.sh) in modo che la shell passi il proprio PID al binario. Quando possibile, evita del tutto il wrapper e chiama il binario direttamente.

Sulla gestione dei riavvii automatici, Restart=on-failure è la scelta più sensata per la maggior parte dei servizi (riavvia su codici di uscita diversi da zero, segnali o timeout), mentre Restart=always va usato con cautela. RestartSec=5 aggiunge un ritardo prima del riavvio: senza, un servizio rotto martella il sistema in loop.

Override sicuri: mai toccare i file dei pacchetti

Non modificare mai i file in /lib/systemd/system/: gli aggiornamenti dei pacchetti sovrascrivono le modifiche. Il modo corretto è usare i drop-in override:

systemctl edit nginx

Questo comando apre un editor e crea automaticamente un file in /etc/systemd/system/nginx.service.d/override.conf, dove inserire solo le direttive da modificare:

[Service]
LimitNOFILE=65536
Restart=on-failure

Per vedere l’unit completo con gli override applicati: systemctl cat nginx. Dopo ogni modifica a un unit file, esegui sempre systemctl daemon-reload prima di riavviare il servizio: dimenticarlo è una fonte comune di confusione quando le modifiche non sembrano avere effetto.

Hardening: sandboxing gratuito integrato in systemd

systemd offre funzionalità di sandboxing native che riducono la superficie d’attacco in caso di compromissione del servizio. Da aggiungere nella sezione [Service]:

# Impedisce l'acquisizione di nuovi privilegi
NoNewPrivileges=yes

# /tmp privato e isolato
PrivateTmp=yes

# Accesso in sola lettura a /usr, /boot, /etc
ProtectSystem=strict

# Impedisce la scrittura nelle home directory
ProtectHome=yes

# Restringe le famiglie di indirizzi utilizzabili
RestrictAddressFamilies=AF_INET AF_INET6

# Limita le syscall a un set sicuro
SystemCallFilter=@system-service

Parti da PrivateTmp=yes e NoNewPrivileges=yes, che sono a costo pressoché zero. Aggiungi le altre opzioni con cautela, in particolare ProtectSystem=strict, che richiede che l’applicazione scriva solo in /var, /tmp o percorsi esplicitamente consentiti tramite ReadWritePaths=. Per servizi esposti su internet, questo è un investimento minimo con un ritorno di sicurezza notevole.

systemd timer: un sostituto moderno di cron

I timer di systemd sono un’alternativa spesso sottovalutata a cron, con logging integrato, gestione delle dipendenze ed esecuzione “catch-up” se il sistema era spento all’orario previsto. Servono due file: il timer e il service corrispondente.

/etc/systemd/system/backup.service:

[Unit]
Description=Nightly Backup

[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh

/etc/systemd/system/backup.timer:

[Unit]
Description=Run backup nightly at 2am

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true fa sì che, se il sistema era spento alle 2:00, il job venga eseguito al successivo avvio: qualcosa che cron non fa senza configurazioni aggiuntive. Si abilita e avvia il timer, non il service:

systemctl enable --now backup.timer
systemctl list-timers

Troubleshooting sistematico dei servizi falliti

Quando un servizio fallisce, conviene seguire un percorso ripetibile invece di procedere per tentativi.

1. Controlla lo stato

systemctl status myapp

Mostra stato corrente, ultime righe di log e PID. Un servizio fallito appare tipicamente così:

● myapp.service - My Python Web App
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
     Active: failed (Result: exit-code) since Tue 2026-05-05 14:22:01 UTC
    Process: 1234 ExecStart=/opt/myapp/venv/bin/python app.py (code=exited, status=203/EXEC)

status=203/EXEC indica che il binario non poteva essere eseguito: quasi sempre un problema di percorso o permessi.

2. Leggi il journal

journalctl -u myapp -n 50      # ultime 50 righe
journalctl -u myapp -f         # segui in tempo reale
journalctl -u myapp -b         # dall'ultimo boot
journalctl -u myapp -b -1      # dal boot precedente

3. Verifica errori di sintassi

systemd-analyze verify /etc/systemd/system/myapp.service

Intercetta typo, direttive sconosciute e dipendenze mancanti prima ancora di tentare l’avvio.

4. Testa manualmente ExecStart

sudo -u webapp /opt/myapp/venv/bin/python app.py

Se fallisce qui, il problema è nell’applicazione o nel suo ambiente, non in systemd.

Pattern di errore comuni

  • status=203/EXEC: binario non trovato o non eseguibile
  • status=217/USER: l’utente specificato in User= non esiste
  • status=200/CHDIR: WorkingDirectory non esiste o non è accessibile
  • Start request repeated too quickly: crash-loop; aggiungi RestartSec= e resetta il contatore con systemctl reset-failed myapp
  • Timeout on start: il servizio non ha segnalato la propria disponibilità in tempo; verifica se Type= è corretto (un demone che fa fork con Type=simple va cambiato in Type=forking)

Un esempio production-ready

Ecco un unit file completo per un’API Node.js che riassume le best practice viste finora:

[Unit]
Description=Node.js API Server
Documentation=https://github.com/example/myapi
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/myapi
ExecStart=/usr/bin/node /opt/myapi/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
StartLimitBurst=3
StartLimitIntervalSec=60
EnvironmentFile=/etc/myapi/env
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapi

# Hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/myapi /var/log/myapi

[Install]
WantedBy=multi-user.target

StartLimitBurst=3 e StartLimitIntervalSec=60 insieme significano: se il servizio si riavvia più di 3 volte in 60 secondi, systemd smette di riprovare, evitando che un servizio rotto continui a “sbattere” contro il sistema.

Comandi rapidi da tenere a portata di mano

# Start, stop, restart, reload
systemctl start myapp
systemctl stop myapp
systemctl restart myapp
systemctl reload myapp

# Abilitazione al boot
systemctl enable myapp
systemctl disable myapp

# Unit completo con override applicati
systemctl cat myapp

# Servizi falliti
systemctl --failed

# Performance di boot
systemd-analyze blame
systemd-analyze critical-chain

Conclusione

Scrivere un service unit non è complicato una volta capito cosa fa ciascuna sezione: [Unit] gestisce ordinamento e dipendenze, [Service] definisce come il processo gira e si riprende dai fallimenti, [Install] controlla il comportamento al boot. Le opzioni di hardening richiedono pochi minuti in più ma vanno inserite di default su ogni servizio esposto in rete. E se ancora lanci job da cron, vale la pena provare un timer systemd: l’integrazione con il journal da sola giustifica il passaggio.

Fonte: LinuxBlog.io – systemd Services: Writing, Managing, and Troubleshooting Unit Files on Linux

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su systemd: guida completa a unit file, hardening e troubleshooting dei servizi Linux, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community