Systemd non avvia l'unità: leggi il codice di uscita
Leggi prima systemctl status: scopri cosa significano 203/EXEC e 226/NAMESPACE e perché un'unità può avviarsi correttamente e terminare dopo un secondo.
Perché un'unità systemd non si avvia
Un'unità systemd che non si avvia indica la causa in un campo. Esegui systemctl status <unit> e cerca code= e status= nella riga che segnala l'errore. Un codice di stato nell'intervallo 200 indica che systemd non ha mai avviato il programma: l'errore si è verificato durante la preparazione dell'ambiente richiesto dal file dell'unità. Un codice inferiore a 200 indica che il programma è stato eseguito e si è chiuso autonomamente. In questo caso, probabilmente il file dell'unità è corretto e il problema riguarda l'applicazione.
Questa distinzione determina il percorso di diagnosi. Tutto ciò che segue deriva da questa distinzione, nell'ordine indicato dai codici.
Quali tre comandi rispondono alla domanda, nell’ordine
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl status dà il verdetto. Leggi prima la riga Loaded:, perché indica il file che systemd ha effettivamente analizzato e specifica se l’unità è abilitata, mascherata oppure non è stata trovata. Leggi quindi la riga Active: e la coppia code= e status= riportata sotto.
journalctl -u myapp.service -b --no-pager fornisce i dettagli. -u filtra l’output limitandolo a quella specifica unità, -b limita l’analisi all’avvio corrente, così non leggi un errore della settimana precedente, e --no-pager stampa direttamente sul terminale, in modo da poter inoltrare l’output a grep. status mostra soltanto le ultime righe del log e accorcia quelle troppo lunghe. Il journal mostra tutto ciò che il programma ha stampato prima di terminare, che di solito contiene l’errore reale. Aggiungi -n 100 per ottenere una cronologia più ampia oppure esegui il comando con -f in un secondo terminale mentre riavvii l’unità.
systemd-analyze verify carica un file di unità senza eseguirlo. Segnala le sezioni e le direttive sconosciute e indica i comandi in ExecStart= che non può eseguire. In questo modo rileva due classi di errori spesso silenziosi: una chiave scritta in modo errato, che systemd ignora al caricamento mostrando un avviso che quasi nessuno legge, e un percorso inesistente.
Dopo aver modificato un file di unità, esegui sudo systemctl daemon-reload. Fino a quel momento, systemd continua a usare la copia caricata in precedenza e systemctl status aggiunge un avviso che il file sul disco è cambiato. Una correzione che «non ha avuto effetto» spesso è semplicemente una correzione che systemd non ha ancora riletto.
Altri due comandi sono utili. systemctl cat myapp.service stampa l’unità effettiva, cioè il file principale insieme a ogni drop-in presente in /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory stampa quei valori nel formato analizzato da systemd, ossia la configurazione che verrà realmente eseguita.
Che cosa significa status=203/EXEC?
203/EXEC indica che systemd ha completato la configurazione, ha chiamato execve() e il kernel ha rifiutato l'esecuzione. Il programma non ha eseguito alcuna istruzione del proprio codice. Quattro cause coprono quasi tutti i casi.
- Il percorso in
ExecStart=è errato oppure non è assoluto. Verificalo conls -le confrontalo con la stringa esatta presente nel file unit. - Il file non ha il bit di esecuzione.
sudo chmod +x /opt/myapp/run.shlo imposta. Un file estratto da un archivio o copiato da un altro computer spesso perde questo bit. - La riga shebang è errata. Il kernel legge la prima riga di uno script ed esegue l'interprete indicato. Per questo
#!/usr/bin/env python3fallisce quando il PATH del servizio non contienepython3. Inoltre, un file salvato con terminatori di riga Windows richiede un interprete chiamato/bin/bash\r, che non esiste. - Il file non è eseguibile su questo computer: ha un'architettura errata oppure è un file di testo privo di shebang.
Riproduci il problema manualmente, usando l'utente del servizio, prima di modificare qualsiasi cosa.
sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -Afile indica l'architettura e mostra "with CRLF line terminators" quando il problema riguarda i terminatori di riga. cat -A mostra lo stesso problema sotto forma di ^M finale. Rimuovili con sed -i 's/\r$//' /opt/myapp/run.sh.
Una precisazione importante sull'intervallo: 200 e valori superiori sono una convenzione, non una garanzia. Il tuo programma può terminare con 203 e systemd non è in grado di distinguere i due casi. systemd-analyze exit-status 203 stampa il nome e la classe di qualsiasi codice; questo aiuta a leggere la tabella. Tuttavia, se l'applicazione utilizza codici di uscita superiori a 199, modificali.
Perché ricevo 217/USER o 216/GROUP?
217/USER indica che l'account specificato in User= non esiste quando il servizio viene avviato. 216/GROUP indica lo stesso errore per Group= o SupplementaryGroups=. Verificatelo con un comando per ciascun caso.
getent passwd appuser
getent group appgroupOgni comando stampa una riga oppure non stampa nulla e restituisce un codice diverso da zero. Nessun output significa che il nome non è noto al sistema. Di conseguenza, systemd non può eseguire il cambio di account e interrompe l'avvio prima di exec. La soluzione consiste nel creare l'account, non nell'impostare User=root. Eseguire ogni servizio con un account di sistema dedicato con privilegi minimi è esattamente lo scopo di questa direttiva.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes evita il problema perché fa in modo che systemd assegni un account temporaneo a ogni avvio. È adatto a un servizio che non conserva dati persistenti. Qualsiasi servizio che scrive file richiede anche StateDirectory=, perché l'ID utente cambia tra un avvio e l'altro e i file presenti in un percorso normale finiscono di proprietà di un account che non esiste più.
Che cos’è 226/NAMESPACE?
226/NAMESPACE deriva dalle direttive di sandboxing. Quando un’unità imposta ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= o direttive simili, systemd crea un mount namespace privato per il servizio prima di eseguire il programma. In questo contesto, un namespace è una vista privata del filesystem per un singolo processo. Se un mount previsto dal piano non riesce, l’avvio termina con il codice 226 e il programma non viene mai eseguito.
La causa più comune è un percorso in ReadWritePaths= che non esiste. ProtectSystem=strict monta l’intero filesystem in sola lettura, mentre ReadWritePaths= riapre in scrittura i percorsi indicati. systemd non può riaprire una directory che non esiste. Sono disponibili due correzioni appropriate. Lasciare che systemd crei la directory con StateDirectory=, che crea /var/lib/<name> a ogni avvio e la assegna all’utente del servizio, oppure anteporre il prefisso - al percorso, indicando a systemd di ignorare la voce quando la sorgente non è disponibile. La correzione sbagliata consiste nel rimuovere il blocco di hardening, trasformando un problema di cinque minuti in un problema permanente.
[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploadsQuando non è possibile capire quale riga sia responsabile, rimuovere l’intero blocco di hardening, ricaricare la configurazione e avviare il servizio. Se il servizio parte, aggiungere nuovamente le righe una alla volta e riavviare dopo ciascuna modifica. Due codici correlati sono 233/RUNTIME_DIRECTORY e 238/STATE_DIRECTORY. Indicano che systemd non ha potuto creare o assegnare la proprietà della directory indicata in RuntimeDirectory= o StateDirectory=, di solito perché il percorso esiste già ed è assegnato a un altro utente.
Perché compare 200/CHDIR quando WorkingDirectory sembra corretto?
200/CHDIR indica che chdir() in WorkingDirectory= non è riuscito. La directory non esiste oppure l’utente del servizio non può accedervi. Per accedere a una directory è necessaria l’autorizzazione di esecuzione sulla directory e su ogni directory padre del percorso. Di conseguenza, un /home/deploy/app perfettamente leggibile non è raggiungibile quando /home/deploy ha modalità 700 e il servizio viene eseguito come appuser.
sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myappnamei -l mostra il proprietario e la modalità di ogni componente del percorso. È il modo più rapido per individuare la directory che impedisce l’accesso al resto del percorso. Impostare WorkingDirectory=-/srv/myapp rende non fatale l’assenza della directory. È corretto per un programma che non dipende dalla directory iniziale, ma non per un programma che apre file usando percorsi relativi.
Perché il servizio si avvia e poi si arresta un secondo dopo?
Qui non compare alcun codice della serie 200 e spesso non viene mostrato neppure un messaggio di errore. L’unità mostra inactive (dead) subito dopo l’avvio oppure passa ripetutamente da activating (auto-restart). systemd ha configurato correttamente l’ambiente. Il problema è la differenza tra il comportamento effettivo del programma e quello dichiarato da Type=.
Type=simple, il valore predefinito, indica che il programma resta in primo piano. Se gli si assegna un demone che crea un processo in background e termina, systemd rileva la conclusione del processo principale e considera completato il servizio. La maggior parte dei demoni offre un’opzione per restare in primo piano, ad esempio nginx -g 'daemon off;'.
Type=forking indica che il primo processo termina quando il processo figlio è pronto. Se gli si assegna un programma che resta in primo piano, il job di avvio attende che TimeoutStartSec= scada, dopo 90 secondi per impostazione predefinita; quindi systemd lo termina e registra un timeout.
Type=notify indica che il programma chiama sd_notify() per segnalare di essere pronto. Un programma che non supporta questo meccanismo non invia alcuna segnalazione. L’avvio quindi va in timeout e il journal registra il risultato come errore di protocollo.
Scegliere il tipo in base al comportamento effettivo del programma. Differenze tra simple, forking, oneshot e notify è la decisione che risolve questa intera categoria di errori.
Quando un servizio termina ripetutamente, systemd smette di tentare l’avvio e segnala che la richiesta di avvio è stata ripetuta troppo rapidamente. L’unità resta quindi in stato failed finché non scade la finestra del limite oppure finché non si esegue sudo systemctl reset-failed myapp.service. Aumentare il limite nasconde soltanto il sintomo. Consultare il journal a partire dal primo errore, non dall’ultimo, e leggere che cosa ritenta realmente Restart=on-failure prima di modificarlo.
Perché l’unità è inattiva senza alcun errore?
Un’unità può essere ignorata invece di essere avviata. Le direttive Condition* sono silenziose per progettazione: quando il controllo non viene superato, systemd considera il job riuscito e non esegue alcuna azione. Un’unità che contiene ConditionPathExists=/etc/myapp/config.yml non verrà mai avviata finché il file non esiste e non segnalerà mai un errore.
systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i conditionConditionResult=no conferma l’omissione e il journal indica il controllo che non è stato soddisfatto. Usa invece una direttiva Assert* quando l’assenza di un prerequisito deve generare un errore esplicito. Condizioni, assert e ordinamento delle unità spiega quale controllo utilizzare in ciascun punto.
Esistono anche altri casi silenziosi nelle vicinanze. Un errore “could not be found” indica di solito che il file si trova nella directory sbagliata oppure che non hai ricaricato la configurazione: i file delle unità creati manualmente devono trovarsi in /etc/systemd/system/. Un’unità mascherata rifiuta ogni avvio finché sudo systemctl unmask myapp.service non rimuove la maschera. Inoltre, systemctl enable non funziona su un’unità priva della sezione [Install], quindi aggiungi WantedBy=multi-user.target.
E se il processo fosse stato terminato invece di fallire?
code=killed è un caso diverso da code=exited. Un componente esterno ha terminato il processo. status=9/KILL indica l'intervento dell'out of memory (OOM) killer e il journal riporta il processo selezionato. Un limite impostato manualmente produce lo stesso effetto all'interno del cgroup (control group). Controllate quindi la memoria libera sull'host con free -m e verificate se l'unit dispone di un MemoryMax=. MemoryMax, CPUQuota e gli altri limiti dei cgroup spiega quali limiti terminano un processo e quali lo rallentano soltanto.
status=15/TERM subito dopo un tentativo di avvio indica in genere che systemd ha superato il timeout di avvio e ha terminato il processo. In questo caso occorre tornare a Type=.
Due abitudini che prevengono la maggior parte di questi errori
Usa sempre percorsi assoluti. systemd non esegue la shell di login, quindi non sono disponibili .bashrc, .profile né un ambiente virtuale attivato. $PATH per un servizio di sistema è un elenco incorporato breve che non contiene /opt né gli shim di un gestore delle versioni del linguaggio. Scrivi per esteso /usr/bin/python3 o /opt/myapp/venv/bin/python. command -v myapp nella shell stampa il percorso da copiare. La stessa regola vale per WorkingDirectory=, EnvironmentFile= e per ogni percorso in ReadWritePaths=.
ExecStart= non è una shell. systemd divide la riga in parole e richiama direttamente execve(). Pipe, redirezioni, glob, &&, backtick e ~ non hanno alcun significato: arrivano al programma come argomenti letterali. ExecStart=/usr/bin/myapp --flag > /tmp/out.log passa > e /tmp/out.log a myapp, che quindi termina con un errore di sintassi d'uso apparentemente scollegato da systemd. Quando ti servono funzioni della shell, devi richiedere esplicitamente una shell.
ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'Per il solo output non è necessario farlo. Per impostazione predefinita, l'output del servizio viene inviato al journal e StandardOutput=append:/var/log/myapp.log scrive in un file senza coinvolgere alcuna shell.
L'espansione delle variabili è limitata allo stesso modo. $MYVAR e ${MYVAR} vengono sostituite usando Environment= e EnvironmentFile=; non viene espanso nient'altro. $HOME non è impostata per un servizio di sistema, a meno che non la imposti esplicitamente. Un EnvironmentFile= non è neppure uno script di shell: export non può essere usato al suo interno, le regole di quotatura sono diverse da quelle di bash e un file mancante causa un errore fatale, a meno che il percorso non sia preceduto da -.
Procedura su un server in produzione
Leggi il codice, dimostra la causa, modifica una sola cosa e riavvia. L'ordine è più importante del conoscere ogni numero, perché evita di accumulare tre modifiche speculative e di perdere di vista quale abbia risolto il problema. La stessa procedura funziona anche per le unità che non hai scritto tu. Un timer che non viene mai attivato dipende da un servizio che non è mai stato avviato, quindi esegui prima il debug del servizio: un timer systemd e il servizio che attiva presenta esattamente i problemi descritti sopra, mentre il timer nasconde l'output finché non lo richiedi al journal.
FAQ
Che cosa significa status=203/EXEC in systemctl status?
systemd ha configurato tutto ciò che l'unità richiedeva, quindi la chiamata execve() non è riuscita e il programma non è mai stato avviato. Controllate questi quattro elementi, nell'ordine: il percorso in ExecStart= esiste ed è assoluto, il file ha il bit di esecuzione, lo shebang specifica un interprete presente nel PATH del servizio e il file usa terminatori di riga Unix. Per l'ultimo controllo, file restituisce "with CRLF line terminators"; in questo modo il nome dell'interprete diventa /bin/bash\r e il kernel rifiuta l'esecuzione.
Perché il servizio si avvia e poi si arresta subito?
Il file dell'unità dichiara un comportamento che il programma non ha. Con Type=simple systemd si aspetta che il programma resti in primo piano; quindi un daemon che esegue il fork in background risulta terminato non appena esegue il fork. Con Type=forking systemd attende l'uscita del primo processo; di conseguenza, un programma in primo piano mantiene il job di avvio in attesa finché TimeoutStartSec= non scade. Impostate Type= in base al comportamento del programma e, se il programma offre un flag per il primo piano, usate quel flag insieme al valore predefinito Type=simple.
Come posso vedere l'errore effettivo invece dell'output breve di status?
systemctl status stampa soltanto le ultime righe del journal e accorcia quelle troppo lunghe. Eseguite journalctl -u myapp.service -b --no-pager per ottenere tutto ciò che l'unità ha registrato durante questo boot, aggiungete -n 200 per ampliare l'intervallo oppure inoltrate l'output a grep. Se l'applicazione scrive un proprio file di log, leggete anche quello, perché systemd acquisisce soltanto ciò che il programma invia allo standard output e allo standard error.
Perché la mia unità è inattiva senza mostrare messaggi di errore?
Nella maggior parte dei casi, una direttiva Condition* ne ha impedito l'avvio. Questi controlli sono silenziosi: una condizione non soddisfatta considera riuscito il job di avvio. Eseguite systemctl show myapp.service -p ConditionResult e cercate ConditionResult=no, quindi leggete la riga del journal che identifica il controllo. L'altra causa comune è un'unità mascherata, che rifiuta ogni avvio finché sudo systemctl unmask non rimuove la maschera.
Devo eseguire daemon-reload dopo ogni modifica al file dell'unità?
Sì, per qualsiasi modifica a un file dell'unità o a un drop-in. sudo systemctl daemon-reload fa rileggere a systemd i file dal disco, quindi sudo systemctl restart myapp.service applica le modifiche al servizio in esecuzione. Non è necessario dopo systemctl edit, che esegue automaticamente il reload, né dopo la modifica di un file di configurazione appartenente all'applicazione anziché a systemd.