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 processoExecStart. 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 semprePIDFile=.Type=notify— il processo notifica la propria disponibilità viasd_notify(). Più affidabile disimpleper applicazioni complesse.Type=oneshot— per script che eseguono e terminano. AggiungiRemainAfterExit=yesse vuoi che risulti “active” anche dopo l’uscita.Type=exec(systemd 240+) — simile asimple, ma systemd attende l’effettivaexecve()del binario prima di considerare il servizio avviato, intercettando i casi in cuiExecStartnon 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:
WorkingDirectorynon esiste o non è accessibile - Start request repeated too quickly: crash-loop; aggiungi
RestartSec=e resetta il contatore consystemctl 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 conType=simpleva cambiato inType=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