1 03/11/2026 11 min

Un servizio systemd è il modo più pulito per far partire un processo Linux in modo controllato, riavviarlo se cade, agganciarlo al boot e gestirlo in modo coerente con il resto del sistema. In pratica, invece di lanciare manualmente uno script da shell o infilare un comando in cron, descrivi cosa deve fare il servizio, quando deve partire e come deve comportarsi in caso di errore.

Il punto forte di systemd non è solo l’avvio automatico. È soprattutto l’osservabilità: stato del servizio, log centralizzati, dipendenze esplicite, limiti di risorse, restart policy e integrazione con il resto della macchina. Se il servizio è scritto bene, diventa più semplice da mantenere, più prevedibile in produzione e più facile da diagnosticare quando qualcosa non torna.

Quando ha senso creare un’unità systemd

Ha senso quando devi gestire un demone o uno script che deve rimanere attivo, ripartire al boot, essere monitorato da systemd e avere un ciclo di vita chiaro. È il caso tipico di applicazioni web, worker, agent di monitoraggio, microservizi, job schedulati persistenti o wrapper che avviano un processo esterno.

Non è invece la scelta giusta per un comando usa-e-getta, per un’attività che dura pochi secondi o per un task che deve essere eseguito una sola volta e poi terminare. In quel caso è meglio valutare un timer systemd, un cron job o un’unità di tipo oneshot, a seconda del comportamento desiderato.

Struttura di base di un file unit

Le unit systemd sono file di testo con estensione .service, di solito collocati in /etc/systemd/system/ per le unit personalizzate dell’amministratore, oppure in /usr/lib/systemd/system/ o percorsi simili per quelle fornite dai pacchetti. La scelta corretta, per servizi creati a mano, è quasi sempre /etc/systemd/system/, così il file resta sotto controllo locale e non viene sovrascritto dagli aggiornamenti dei pacchetti.

Un servizio minimale ha almeno tre sezioni: [Unit] per descrizione e dipendenze, [Service] per il comportamento del processo, [Install] per il collegamento al boot. Senza [Install] puoi comunque avviare il servizio manualmente, ma non potrai abilitarlo con systemctl enable finché non specifichi un target di riferimento.

Esempio completo: servizio per uno script Python

Immaginiamo di voler eseguire un’applicazione Python che gira in foreground e deve restare attiva come servizio. Il binario o lo script potrebbe trovarsi in /opt/myapp/app.py, con log scritti su journal e avvio automatico al boot.

Prima di tutto conviene verificare che il comando di avvio funzioni da shell con l’utente corretto. Se il servizio in produzione userà un account dedicato, prova il comando con quel contesto prima di costruire l’unità, così isoli subito problemi di permessi, path mancanti o dipendenze non installate.

Supponiamo che il servizio debba essere eseguito come utente myapp e che il comando di start sia /usr/bin/python3 /opt/myapp/app.py. Un file unit ragionevole potrebbe essere questo:

[Unit]
Description=MyApp service
After=network-online.target
Wants=network-online.target [Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1 [Install]
WantedBy=multi-user.target

Qui ci sono alcuni dettagli importanti. Type=simple è adatto ai processi che restano in foreground e non fanno daemonizzazione autonoma. Restart=on-failure dice a systemd di riprovare se il processo esce in errore, ma non in caso di stop volontario. After=network-online.target e Wants=network-online.target sono utili se il servizio dipende davvero dalla rete pronta, ma non sostituiscono il controllo delle dipendenze applicative.

Se l’applicazione necessita di variabili d’ambiente più articolate, è meglio usare un file separato, per esempio /etc/default/myapp o /etc/sysconfig/myapp, e richiamarlo con EnvironmentFile=. In questo modo il file unit resta più pulito e puoi cambiare configurazione senza toccare la logica di avvio.

Creazione del file unit passo per passo

La procedura pratica è questa. Prima definisci l’utente di esecuzione, poi il comando esatto, poi il working directory e infine le policy di restart. È un errore comune scrivere subito il file e accorgersi dopo che il processo non trova file relativi, socket o directory di runtime.

  • Crea l’utente dedicato, se non esiste già, ad esempio myapp. Questo riduce il blast radius: se il processo viene compromesso, non gira con privilegi inutili.

  • Prepara la directory dell’applicazione, per esempio /opt/myapp, e verifica che il processo possa leggere i file necessari e scrivere solo dove serve davvero.

  • Scrivi il file /etc/systemd/system/myapp.service con la configurazione minima valida.

  • Ricarica la configurazione di systemd con systemctl daemon-reload prima di provare l’avvio.

  • Avvia il servizio e controlla stato e log con systemctl status myapp e journalctl -u myapp -n 50 --no-pager.

  • Il comando di reload è fondamentale: systemd non rilegge automaticamente i file unit dopo una modifica. Se dimentichi questo passaggio, rischi di inseguire un comportamento vecchio anche se il file sul disco è corretto.

    Avvio, abilitazione e verifica operativa

    Una volta salvato il file, il ciclo corretto è sempre lo stesso: ricarichi systemd, avvii il servizio, ne controlli lo stato e solo dopo lo abiliti al boot. La sequenza tipica è questa:

    systemctl daemon-reload
    systemctl start myapp
    systemctl status myapp
    systemctl enable myapp

    Se il servizio non parte, la prima fonte di verità è systemctl status myapp, che mostra l’ultimo stato e spesso un estratto del journal. Se l’errore è più ambiguo, passa a journalctl -u myapp --since "10 minutes ago" per vedere l’intera sequenza degli eventi recenti.

    Un controllo che vale sempre la pena fare è il processo effettivamente in esecuzione. Se il servizio dichiara active (running) ma l’applicazione non risponde come dovrebbe, verifica che il PID riportato sia quello atteso e che il processo stia ascoltando sulla porta giusta o stia scrivendo nel socket corretto.

    Tipo di servizio: simple, oneshot, notify, fork

    La scelta di Type= cambia il modo in cui systemd interpreta l’avvio. simple è il caso più comune: systemd considera il servizio avviato quando il comando è partito. È la scelta naturale per processi in foreground. oneshot serve per attività che eseguono un’azione e poi terminano, come script di inizializzazione. notify è utile quando il processo sa comunicare a systemd che è pronto solo dopo aver completato l’inizializzazione. fork è più adatto a vecchi demoni che si staccano dal terminale e fanno fork esplicito, ma oggi è spesso meno desiderabile di un processo in foreground ben gestito.

    Se controlli il software che stai impacchettando, la soluzione migliore è quasi sempre farlo restare in foreground e usare Type=simple o, se il programma supporta il signaling corretto, Type=notify. Questo rende il comportamento più trasparente e la diagnosi molto più semplice.

    Restart policy e gestione dei failure

    La parte che fa la differenza in produzione è la politica di restart. Restart=on-failure è spesso la scelta più equilibrata: riavvia il servizio quando esce con errore o viene terminato in modo anomalo, ma non entra in loop per uno stop amministrativo. Restart=always è più aggressivo e va usato con attenzione, perché può ostacolare uno stop manuale se non si conoscono bene i segnali e i tempi di attesa.

    Puoi anche regolare il backoff con RestartSec=. Se un servizio crasha subito al boot, un intervallo troppo corto può creare una tempesta di restart e saturare log e CPU. In quei casi, inserire qualche secondo di pausa è una misura semplice e spesso efficace.

    Per evitare loop infiniti, systemd dispone anche di limiti come StartLimitBurst e StartLimitIntervalSec. Sono utili quando vuoi impedire che un servizio fallito riparta all’infinito senza lasciare tempo al sistema o all’operatore di intervenire.

    Dipendenze, ordine di avvio e target

    Molti problemi non dipendono dal file unit in sé, ma dall’ordine di avvio. Se il servizio ha bisogno del database, della rete o di un filesystem montato, queste dipendenze devono essere esplicitate. Un After= indica l’ordine, ma non garantisce da solo che il servizio dipendente sia disponibile; per questo spesso si usa anche Requires= o Wants=, a seconda del grado di vincolo desiderato.

    Per esempio, se il servizio richiede davvero una mount point specifica, puoi aggiungere RequiresMountsFor=/opt/myapp. Se invece basta che la rete sia pronta prima dell’avvio, After=network-online.target è una scelta sensata, purché il sistema sia configurato per raggiungere davvero quel target in modo affidabile.

    Il target di installazione più comune per servizi di sistema è multi-user.target. In pratica equivale al livello di esecuzione normale del sistema server, cioè quando la macchina è pronta a ospitare servizi persistenti.

    Variabili d’ambiente e file esterni

    Per configurazioni non banali, conviene separare il file unit dai parametri applicativi. Un esempio tipico è questo:

    [Service]
    EnvironmentFile=-/etc/default/myapp
    ExecStart=/usr/bin/python3 /opt/myapp/app.py

    Il prefisso - davanti al path indica che il file è opzionale. Se il file non esiste, il servizio parte comunque. Questa soluzione è utile quando vuoi mantenere il file unit portabile e mettere i parametri in un punto separato, più facile da gestire con configurazioni locali o strumenti di automazione.

    Se invece il file è obbligatorio, togli il trattino. In quel caso, la mancanza del file farà fallire l’avvio, ed è spesso la scelta giusta quando vuoi evitare configurazioni incomplete o ambienti parzialmente preparati.

    Log, troubleshooting e diagnosi rapida

    Quando un servizio non parte, il primo controllo è quasi sempre il journal. I comandi più utili sono questi:

    systemctl status myapp
    journalctl -u myapp -b --no-pager
    journalctl -u myapp -f

    -b restringe ai log del boot corrente, utile quando stai indagando un problema comparso dopo il riavvio. -f segue i log in tempo reale, comodo durante i test di start e restart. Se il servizio fallisce subito, spesso il messaggio decisivo è già nelle prime righe di journal: path sbagliato, permesso negato, dipendenza assente, porta occupata, file di configurazione malformato.

    Un errore molto comune è un ExecStart puntato al binario sbagliato o a uno script senza permessi di esecuzione. In quel caso, il fix è banale ma va fatto in modo pulito: verifica il path reale con which o con il package manager della distro, controlla i permessi con ls -l e, se serve, correggi il file unit invece di improvvisare con wrapper fragili.

    Un servizio ben scritto non deve limitarsi a partire. Deve anche ridurre il più possibile la superficie d’attacco. Systemd offre diversi parametri utili per il rafforzamento del servizio: NoNewPrivileges=true, PrivateTmp=true, ProtectSystem=full, ProtectHome=true, CapabilityBoundingSet= e AmbientCapabilities= quando servono capacità specifiche.

    Non è una checklist da applicare alla cieca. Ogni opzione va testata, perché un hardening troppo aggressivo può rompere scritture legittime, accesso a directory o uso di socket. Il criterio corretto è partire dal minimo necessario, osservare il comportamento e poi restringere gradualmente, verificando ogni volta che il servizio continui a funzionare.

    Per esempio, se il processo deve solo leggere file e ascoltare su una porta, spesso è possibile limitarlo parecchio. Se invece deve scrivere in cache, usare directory temporanee o interagire con dispositivi specifici, le restrizioni vanno calibrate con attenzione. Il vantaggio è concreto: meno privilegi significa meno danni potenziali in caso di bug o compromissione.

    Override e drop-in senza modificare il file principale

    Quando vuoi cambiare solo un dettaglio di un servizio già esistente, meglio evitare di toccare il file originale se è gestito da un pacchetto. In quel caso si usa un drop-in tramite systemctl edit nome-servizio, che crea un override in /etc/systemd/system/nome-servizio.service.d/override.conf.

    Questo approccio è molto più sicuro in ambienti gestiti, perché separa le personalizzazioni locali dalla configurazione di base. Se il pacchetto viene aggiornato, il tuo override resta intatto. È anche più leggibile, perché ogni modifica amministrativa è concentrata in un file dedicato.

    Un esempio di drop-in per cambiare solo il comando di avvio potrebbe essere questo:

    [Service]
    ExecStart=
    ExecStart=/usr/bin/python3 /opt/myapp/app.py --config /etc/myapp/config.yml

    La riga vuota ExecStart= serve a cancellare la definizione precedente prima di impostarne una nuova. È un dettaglio essenziale: senza di esso, systemd considererebbe entrambe le direttive e l’unità fallirebbe per configurazione incoerente.

    Approccio corretto per servizi custom in produzione

    Il modo giusto di progettare un servizio systemd è pensarlo come un contratto operativo. Devi sapere chi lo esegue, da dove parte, quali risorse usa, cosa succede se fallisce e come lo riporti in uno stato noto. Se questi punti non sono chiari, il file unit diventa solo un wrapper fragile attorno a un comando qualunque.

    La sequenza più solida resta sempre la stessa: prova il comando a mano, costruisci l’unità minima, verifica i log, aggiungi restart policy, poi hardening e dipendenze. Se qualcosa si rompe, torna indietro con un file unit semplice e leggibile invece di accumulare correzioni opache. In ambienti reali, la semplicità ben documentata vale più di una configurazione teoricamente elegante ma difficile da diagnosticare.

    Se il servizio è critico, tieni sempre una copia della versione precedente del file unit o un diff chiaro delle modifiche. In questo modo il rollback è immediato: ripristini il file, fai systemctl daemon-reload e riavvii il servizio. È una misura semplice, ma spesso è quella che salva tempo quando il problema emerge in finestra di manutenzione o durante un deploy.