SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-21

Dipendenze e condizioni di systemd: guida al debug

Requires, Wants, After, Before, ExecStartPre e Condition hanno ruoli diversi. Scopri cosa garantiscono e come diagnosticare un'unita che non parte.

Requires non significa After

Le dipendenze e le condizioni di systemd sono quattro meccanismi distinti che molti file di unità usano come se fossero uno solo. Requires= e Wants= stabiliscono quali altre unità vengono incluse. After= e Before= stabiliscono l'ordine di avvio delle unità. ExecStartPre= esegue un controllo che può causare il fallimento dell'unità. Le famiglie Condition e Assert stabiliscono se l'unità viene eseguita. Ogni meccanismo è indipendente dagli altri. Un'unità può quindi richiedere un'altra unità e avviarsi comunque nello stesso momento.

Questa ultima frase è all'origine di quasi tutte le segnalazioni del tipo: «Funziona quando la avvio manualmente, ma fallisce durante il boot».

[Unit]
Description=Inventory API
Requires=postgresql.service

[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-api

Requires=postgresql.service include PostgreSQL nella stessa transazione di avvio. Non attende però che PostgreSQL sia pronto. systemd avvia entrambi i job in parallelo. Di conseguenza, pg_isready viene eseguito mentre PostgreSQL sta ancora aprendo la propria directory dei dati. Il comando termina con il codice 2 perché nessun processo è ancora in ascolto. L'unità fallisce prima che venga mai raggiunto ExecStart. L'esecuzione di sudo systemctl start inventory-api un'ora dopo funziona perché PostgreSQL è già attivo. Nel file dell'unità non è cambiato nulla. Per questo il file sembra innocuo.

La correzione richiede una sola riga.

[Unit]
Requires=postgresql.service
After=postgresql.service

Nello stesso punto si nasconde un dettaglio importante. Una dipendenza Requires= che fallisce impedisce l'avvio dell'unità soltanto se su di essa è impostato anche After=. Senza l'ordinamento, systemd ha già avviato l'unità quando l'altra unità fallisce. Non resta quindi nulla da annullare. Requires= da solo non offre la protezione che ci si aspetta. Scrivere After= accanto a ogni Requires= e a ogni Wants=, salvo un motivo specifico per non farlo.

Cosa garantiscono Requires, Wants, Requisite e BindsTo

Sono tutte impostazioni di dipendenza. Nessuna di esse definisce l'ordine di avvio.

  • Wants=: include l'altra unità. Se l'altra unità non si avvia o non esiste, questa unità viene comunque avviata. È ciò che crea systemctl enable, come collegamento simbolico all'interno di una directory .wants/.
  • Requires=: include l'altra unità. Se l'altra unità non si avvia e hai definito anche After=, questa unità non viene avviata. Se in seguito l'altra unità viene arrestata esplicitamente, anche questa unità viene arrestata.
  • Requisite=: non include l'altra unità. Se l'altra unità non è già attiva, questa unità termina immediatamente con un errore.
  • BindsTo=: funziona come Requires=, ma questa unità viene arrestata ogni volta che l'altra unità si arresta per qualsiasi motivo, anche quando l'hardware scompare.
  • PartOf=: l'arresto e il riavvio vengono propagati dall'altra unità a questa unità. L'avvio non viene propagato.
  • Conflicts=: l'avvio di questa unità arresta l'altra.

Per un daemon che comunica con un altro daemon, Wants= insieme a After= è generalmente la combinazione corretta. Requires= collega i cicli di vita: se arresti il database per la manutenzione, anche l'applicazione viene arrestata e non viene riavviata quando il database torna disponibile. Wants= insieme a After= definisce l'ordine di avvio senza creare questo legame, mentre una policy di riavvio gestisce il caso in cui la dipendenza scompaia in seguito.

Vengono inoltre ereditate dipendenze che non hai dichiarato. Con DefaultDependencies=yes, che è l'impostazione predefinita, un servizio ordinario riceve automaticamente Requires=sysinit.target, After=sysinit.target basic.target e Conflicts=shutdown.target. Per questo un servizio con una sezione [Unit] quasi vuota viene comunque avviato nella fase avanzata del boot e arrestato correttamente durante lo shutdown.

After e Before definiscono l’ordine della transazione, nient’altro

After= e Before= definiscono esclusivamente l’ordine. Non impongono alcun requisito. After=redis.service in un’unità che non richiama Redis in alcun altro modo non produce alcun effetto: se redis.service non fa parte della transazione, non c’è nulla da attendere e l’unità viene avviata immediatamente.

È importante ripeterlo, perché questa è esattamente la forma dell’errore network-online.target descritto più avanti. L’ordinamento attende soltanto le unità che sono già in fase di avvio nella stessa transazione.

La coppia è simmetrica. After=b.service scritto in a.service equivale a Before=a.service scritto in b.service. Usane quindi uno solo e inseriscilo nell’unità di tua proprietà. Durante l’arresto l’ordine viene invertito automaticamente, quindi After=b.service significa anche che l’unità viene arrestata prima di b.service.

After= attende che l’unità abbia «iniziato» e Type= definisce il significato di questo stato

After= attende che l’altra unità abbia completato l’avvio. Il significato di «completato l’avvio» dipende interamente da Type= dell’altra unità.

  • Type=simple: non appena systemd ha eseguito il fork del processo. Il programma potrebbe non avere ancora analizzato la configurazione, tanto meno aperto un socket.
  • Type=exec: non appena execve() ha avuto esito positivo. È un’indicazione leggermente più forte, ma non dice ancora nulla sulla disponibilità del servizio.
  • Type=forking: quando il processo padre originale termina.
  • Type=oneshot: quando il processo termina. In questo caso, «iniziato» significa realmente che il lavoro è terminato.
  • Type=notify: quando il servizio invia READY=1 sul relativo socket di notifica. Questo è l’unico tipo che segnala una disponibilità effettiva.

Di conseguenza, After= su un daemon Type=simple è una garanzia debole. Questa è la seconda metà della race condition del primo esempio. Se l’unità da cui dipendi viene distribuita come Type=simple, stabilire un ordinamento dopo di essa non significa che accetti già connessioni. Esistono due soluzioni corrette. Puoi stabilire l’ordinamento dopo la relativa unità socket, in modo che il kernel accodi le connessioni in ingresso mentre il daemon è ancora in avvio. In alternativa, puoi fare in modo che il tuo servizio riprovi e lasciare che sia la policy di riavvio a gestire il recupero. Il tipo usato da un’unità è visibile in systemctl cat. Prima di fare affidamento sull’ordinamento, conviene leggere l’impostazione Type= e ciò che ogni valore comunica a systemd.

ExecStartPre è un controllo preliminare che può causare il fallimento dell'unità

ExecStartPre= viene eseguito prima di ExecStart=. Se termina con un codice diverso da zero, l'attivazione viene interrotta e l'unità passa a failed. ExecStart= non viene mai eseguito. Questo meccanismo spiega molti casi in cui un'unità fallisce senza che il programma effettivo registri messaggi: il programma non è mai stato avviato.

Aspetti spesso trascurati:

  • Non è una shell. Non supporta pipe, redirezioni, glob, né &&. Il primo token deve essere un percorso assoluto. Racchiudi la riga in /bin/sh -c '...' quando serve la sintassi della shell.
  • Un prefisso - rende non fatale un codice di uscita diverso da zero: ExecStartPre=-/usr/bin/optional-check.
  • Ogni ExecStartPre= deve terminare prima dell'avvio di quello successivo. Non può avviare un processo a esecuzione prolungata.
  • Tutte le righe ExecStartPre= condividono TimeoutStartSec= con ExecStart=. Un controllo preliminare che resta in attesa di un database consuma il timeout di avvio, quindi l'unità fallisce con Result: timeout dopo che start operation timed out. Terminating. compare nel journal.

La riga dell'errore indica il processo di controllo, non il processo principale:

inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.

Leggi attentamente quel nome simbolico. systemd converte i codici di uscita ridotti tramite una tabella fissa, quindi 2 stampa sempre INVALIDARGUMENT, indipendentemente dal significato che il programma gli attribuiva. status=203/EXEC è l'elemento che contiene l'informazione effettiva: systemd non è riuscito a eseguire il binario, perché il percorso è errato oppure il file non è eseguibile.

Non usare ExecStartPre= per creare directory. RuntimeDirectory=, StateDirectory=, LogsDirectory= e CacheDirectory= le creano con proprietario e modalità corretti, mentre RuntimeDirectory= viene rimosso quando il servizio si arresta. Inoltre, si comportano correttamente con DynamicUser=, a differenza di un mkdir scritto manualmente.

Condition è silenziosa. Assert è esplicito.

Le famiglie Condition e Assert eseguono gli stessi test. Differiscono soltanto nel comportamento adottato quando un test non riesce.

Un Condition...= non superato salta l'unità. Il job di avvio viene segnalato come riuscito. L'unità resta inactive (dead), nulla viene contrassegnato come non riuscito, non viene generato alcun alert e il journal registra una sola riga:

Condition check resulted in Inventory API being skipped.

Su systemd 250 e versioni successive, systemctl status stampa direttamente il motivo:

     Active: inactive (dead)
  Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago

La riga indentata sottostante indica la direttiva esatta che non è riuscita, ad esempio ConditionPathExists=/etc/inventory/api.conf was not met.

Un Assert...= non superato fa fallire l'unità. Il journal indica Assertion failed for Inventory API. e l'unità termina in failed (Result: assert), un risultato abbastanza evidente da essere rilevato dal monitoraggio.

Scegli tra le due famiglie chiedendoti che cosa significa il test non superato. Condition significa "questa unità non si applica a questa macchina". Assert significa "questa condizione deve essere vera e, se non lo è, qualcuno deve essere informato". La maggior parte delle unità richiede Condition. Usa Assert soltanto quando non fare nulla in modo silenzioso è peggio di un'unità non riuscita.

La famiglia Condition presenta due insidie.

Primo: una condizione non superata non fa fallire le unità che dipendono da essa. Se a.service contiene Requires=b.service e b.service viene saltata a causa di una condizione, il job di avvio per b.service risulta comunque completato, quindi a.service si avvia normalmente in uno stato in cui b non è in esecuzione. Una condizione protegge soltanto l'unità in cui è definita.

Secondo: le condizioni vengono valutate ogni volta che l'unità viene avviata, nel momento in cui viene eseguito il job. Un'unità attivata da un timer systemd su un VPS può essere saltata cento volte consecutive senza risultare mai non riuscita. Si tratta della stessa classe di operazione a vuoto silenziosa di un job cron che viene eseguito ma non fa nulla; per individuarla, procedi nello stesso modo: leggi il journal dell'unità invece di fidarti del suo stato di uscita.

Le condizioni da conoscere su un server:

  • ConditionPathExists=/etc/inventory/api.conf e la relativa negazione ConditionPathExists=!/etc/inventory/api.conf.
  • ConditionFileNotEmpty= e ConditionDirectoryNotEmpty=, per un file di configurazione o una directory dati che un pacchetto ha creato ma lasciato vuoto.
  • ConditionVirtualization=, in modo che un'unità che richiede un'interfaccia kernel reale possa includere ConditionVirtualization=!container. Verifica ciò che segnala il sistema con systemd-detect-virt.
  • ConditionHost= verifica il nome host o l'ID della macchina; in questo modo lo stesso file di unità condiviso può comportarsi in modo diverso su due server.
  • ConditionKernelCommandLine= e ConditionKernelVersion=, per unità legate a un parametro di avvio o a una versione minima del kernel.

Un'assegnazione vuota cancella l'elenco. È così che un drop-in rimuove una condizione fornita da un pacchetto:

[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.conf

Perché network.target non significa che la rete sia attiva

network.target è un punto di sincronizzazione, non uno stato. Durante l'avvio, ordinare un'unità dopo questo target significa che il software di gestione della rete è stato avviato. Non significa che un'interfaccia disponga di un indirizzo o che esista una route verso Internet. Il target serve soprattutto nella direzione opposta: un'unità ordinata After=network.target viene arrestata prima che la rete venga disattivata durante lo spegnimento.

network-online.target è il target che attende. Si basa su un servizio wait-online appartenente al gestore di rete in uso:

  • systemd-networkd-wait-online.service quando i collegamenti sono gestiti da systemd-networkd, il caso normale su un server Ubuntu configurato tramite netplan.
  • NetworkManager-wait-online.service con NetworkManager.

Le configurazioni ifupdown più vecchie ottengono lo stesso effetto usando invece networking.service. In ogni caso, per usare correttamente il target servono due righe, non una.

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

network-online.target non fa parte della transazione di avvio predefinita e nessuna unità lo include automaticamente. Se scrivi soltanto After=, stai definendo un ordinamento rispetto a un'unità che non è mai stata messa in coda; di conseguenza, l'ordinamento non produce alcun effetto. È il no-op descritto in precedenza, nella sua forma più costosa. La riga Wants= inserisce il target nella transazione, così la riga After= ha qualcosa da attendere.

Il secondo aspetto da conoscere è che lo stato "online" è definito dall'implementazione di wait-online, non da systemd. systemd-networkd-wait-online restituisce il controllo quando i collegamenti che gestisce raggiungono uno stato configurato. Non verifica che la risoluzione DNS funzioni e non verifica che un host remoto sia raggiungibile.

Questa definizione causa un problema comune sui VPS. Un sistema con una seconda interfaccia per una rete privata, dichiarata in netplan ma senza un indirizzo assegnato, può lasciare il servizio wait in attesa fino alla scadenza del timeout:

systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.

L'avvio richiede due minuti aggiuntivi perché il timeout predefinito è di 120 secondi. Esistono due soluzioni. Contrassegna l'interfaccia inutilizzata come optional: true nel file netplan, in modo che networkd smetta di attenderla. In alternativa, aggiungi un drop-in al servizio wait indicando il collegamento che ti interessa con --interface=, oppure passa --any per restituire il controllo non appena un collegamento è attivo.

Meglio ancora, evita di dover usare il target. Molti servizi vengono ordinati dopo network-online.target soltanto perché si associano a un indirizzo specifico e durante l'avvio falliscono con una riga simile alla seguente:

nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)

Il kernel rifiuta l'associazione perché l'indirizzo non è ancora attivo. Impostando net.ipv4.ip_nonlocal_bind=1, un processo può associarsi a un indirizzo che il sistema non possiede ancora; una policy di riavvio gestisce il resto. Ritardare l'intero avvio in attesa della disponibilità della rete è uno strumento eccessivo per un problema che di solito riguarda un solo socket.

Come leggere le dipendenze effettive di systemd su un sistema in esecuzione

Non basarti mai soltanto sul file dell'unità. I drop-in, i symlink .wants/ e le dipendenze predefinite implicite aggiungono collegamenti che il file non mostra.

systemctl cat inventory-api.service

Questo comando stampa il file dell'unità e tutti i drop-in, nell'ordine in cui vengono applicati, mostrando il percorso sorgente sopra ogni blocco. Eseguilo per primo. Un override di cinque righe in /etc/systemd/system/inventory-api.service.d/ ha la precedenza sul file fornito dal pacchetto e, altrimenti, non è visibile.

systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult

Questo comando stampa i valori risolti, dopo l'applicazione dei drop-in e dopo che systemd ha aggiunto le dipendenze implicite. ConditionResult=no risponde direttamente alla domanda: «l'unità ha segnalato il successo, ma non ha fatto nulla».

systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service

La forma semplice percorre Requires= e Wants= verso il basso. --reverse mostra quali unità attivano la tua unità; in questo modo puoi individuare il target che la avvia durante il boot. --after e --before mostrano l'ordinamento. Sono questi i due elementi da leggere quando devi stabilire se qualcosa ha effettivamente atteso.

journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service

Il secondo comando alterna due unità usando timestamp con precisione al millisecondo. In questo modo puoi dimostrare la presenza di una race condition nell'ordinamento invece di limitarti a ipotizzarla. L'errore pg_isready si verifica prima che PostgreSQL registri database system is ready to accept connections, e nell'output è visibile anche l'intervallo tra i due eventi.

systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.service

verify carica l'unità come farebbe systemd e segnala direttive sconosciute, dipendenze da unità inesistenti, cicli di ordinamento e sintassi che non è in grado di analizzare. Non modifica nulla nel sistema. critical-chain stampa la catena di ordinamento che ha ritardato l'unità, indicando il momento in cui ogni passaggio è diventato attivo. Funziona soltanto per un'unità avviata durante il boot corrente.

Dopo aver modificato un file dell'unità, esegui sudo systemctl daemon-reload. Per modificare un'unità fornita da un pacchetto, usa sudo systemctl edit inventory-api.service, che crea automaticamente un drop-in. La modifica del file del vendor in /usr/lib/systemd/system/ funziona fino al successivo aggiornamento del pacchetto, quando il file viene sostituito. Lo stesso meccanismo dei drop-in consente di associare limiti di memoria e CPU a un servizio senza modificare un file di proprietà del pacchetto.

Ordinamento dei cicli e traccia lasciata nel journal

Aggiungi l'ordinamento in entrambe le direzioni e systemd interrompe il ciclo eliminando uno dei job:

systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/start

systemd sceglie quale job eliminare e potrebbe non scegliere quello previsto. Il risultato è un servizio che dopo alcuni riavvii risulta assente e dopo altri presente. Dall'esterno, questo comportamento è molto difficile da diagnosticare. La maggior parte dei cicli deriva da unità che impostano DefaultDependencies=no e poi si ordinano comunque rispetto a basic.target, oppure dall'aggiunta di Before= a un'unità che aveva già After= puntato verso il servizio corrente. systemd-analyze verify li individua senza riavviare il sistema.

L'unità corretta

[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf

[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Ogni riga svolge una sola funzione. Wants= inserisce entrambe le dipendenze nella transazione senza legare a esse il ciclo di vita di questa unità. After= gestisce l'attesa e deve ripetere entrambi i nomi perché dipendenze e ordinamento sono impostazioni separate. ConditionPathExists= indica che una macchina in cui è installato il pacchetto ma manca la configurazione salta l'unità senza generare avvisi: è il comportamento corretto per un servizio basato sulla configurazione. Type=notify fa sì che tutto ciò che è ordinato dopo questa unità attenda la disponibilità effettiva del servizio, non il completamento di un fork. Restart=on-failure gestisce il caso in cui il database non sia più disponibile molto tempo dopo l'avvio, perché l'ordinamento si applica solo al primo avvio. Il livello di aggressività dei nuovi tentativi è determinato da le impostazioni Restart= e RestartSec=.

Verificatela prima di considerarla affidabile:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result

Un'unità in stato corretto mostra ConditionResult=yes con ActiveState=active, mentre Result=success conferma che l'ultima esecuzione non ha prodotto errori. ConditionResult=no insieme a ActiveState=inactive indica che l'unità è stata saltata; la riga del journal che specifica la condizione indica quale test non è stato superato.

FAQ

Requires= attende l'avvio dell'altra unità?

No. Requires= e After= sono impostazioni separate. Requires= include l'altra unità nella stessa transazione, quindi systemd avvia entrambi i job in parallelo. Per imporre l'attesa, aggiungi After= indicando la stessa unità. C'è un secondo motivo per aggiungerla: una dipendenza Requires= che non riesce impedisce l'avvio della tua unità solo quando è impostato anche After=, perché senza un ordinamento la tua unità è già stata avviata quando l'altra non riesce ad avviarsi.

È meglio impostare l'ordinamento dopo network.target o network-online.target?

Durante il boot, network.target indica soltanto che il software di gestione della rete è stato avviato, quindi non garantisce la disponibilità di indirizzi o route. Usa network-online.target quando il servizio richiede un indirizzo funzionante all'avvio e scrivi sia Wants=network-online.target sia After=network-online.target, perché il target non fa parte della transazione di boot predefinita e After= da solo attende un'unità che non è stata accodata. Se il servizio non riesce ad avviarsi soltanto perché si associa a uno specifico indirizzo IP, net.ipv4.ip_nonlocal_bind=1 con Restart=on-failure è una soluzione più leggera rispetto al rinvio del boot.

Perché la mia unità segnala il successo ma non viene mai eseguita?

Un test Condition...= non superato salta l'unità e segnala il job di avvio come riuscito, quindi non viene mai indicato alcun errore. Esegui systemctl show <unit> -p ConditionResult; ConditionResult=no lo conferma. Leggi quindi journalctl -b -u <unit> per trovare la riga Condition check resulted in <description> being skipped. In systemd 250 e versioni successive, systemctl status <unit> indica anche la direttiva esatta che non è stata soddisfatta.

Qual è la differenza tra Condition e Assert?

Eseguono test identici. Un Condition non superato salta l'unità senza messaggi e il job termina comunque con successo. Un Assert non superato fa fallire l'unità, registra Assertion failed for <description>. e la lascia nello stato failed (Result: assert). Usa Condition per indicare che «questa unità non si applica a questa macchina», che copre quasi tutti i casi reali. Usa Assert soltanto quando una precondizione mancante deve essere visibile a chi monitora le unità non riuscite.

Perché ExecStartPre non riesce con status=203/EXEC?

203/EXEC indica che systemd non ha potuto eseguire affatto il comando. Le cause comuni sono un percorso non assoluto, un binario inesistente su quella macchina, un file privo del bit di esecuzione oppure uno script la cui riga #! punta a un interprete mancante. Gli altri codici numerici di systemd derivano da una tabella fissa, quindi status=2/INVALIDARGUMENT indica soltanto che il comando è terminato con il codice 2 e non fornisce informazioni sugli argomenti. Ricorda che ExecStartPre= non viene eseguito tramite una shell, quindi per usare pipe e glob è necessario /bin/sh -c '...'.

#systemd#units#dependencies#ordering#troubleshooting