SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Erreur systemd : comprendre le code de sortie

Lisez d’abord systemctl status : 203/EXEC et 226/NAMESPACE n’ont pas le même sens. Une unité peut aussi démarrer puis s’arrêter une seconde après.

Pourquoi une unité systemd ne démarre pas

Une unité systemd qui ne démarre pas indique la raison dans un champ. Exécutez systemctl status <unit>, puis recherchez code= et status= sur la ligne qui signale l’échec. Un code d’état dans les 200 signifie que systemd n’a jamais lancé votre programme : il a échoué en préparant l’environnement demandé par votre fichier d’unité. Un code inférieur à 200 signifie que votre programme a bien été exécuté, puis s’est arrêté de lui-même. Le fichier d’unité est donc probablement correct et le problème vient de l’application.

Cette distinction définit la méthode de diagnostic. La suite en découle, dans l’ordre indiqué par les codes.

Quels sont les trois commandes qui répondent à la question, dans l’ordre

systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.service

systemctl status donne le verdict. Lisez d’abord la ligne Loaded:, car elle indique le fichier que systemd a effectivement analysé et précise si l’unité est activée, masquée ou introuvable. Lisez ensuite la ligne Active:, puis les lignes code= et status= ci-dessous.

journalctl -u myapp.service -b --no-pager donne les détails. -u filtre sur cette unité uniquement, -b limite la sortie au boot courant pour éviter de lire une erreur de la semaine dernière, et --no-pager affiche directement le résultat dans le terminal afin que vous puissiez l’envoyer vers grep. status affiche uniquement les dernières lignes du journal et tronque les lignes trop longues. Le journal contient tout ce que le programme a affiché avant de s’arrêter. C’est généralement là que se trouve l’erreur réelle. Ajoutez -n 100 pour obtenir davantage d’historique, ou exécutez cette commande avec -f dans un second terminal pendant que vous redémarrez l’unité.

systemd-analyze verify charge un fichier d’unité sans l’exécuter. La commande signale les sections et directives inconnues. Elle signale également les commandes de ExecStart= qu’elle ne peut pas exécuter. Cela détecte les deux catégories d’erreurs silencieuses les plus courantes : une clé mal orthographiée, que systemd ignore au chargement en affichant un avertissement que la plupart des administrateurs ne lisent jamais, et un chemin inexistant.

Après avoir modifié un fichier d’unité, exécutez sudo systemctl daemon-reload. Tant que vous ne l’avez pas fait, systemd continue d’utiliser la copie qu’il a chargée précédemment, et systemctl status ajoute un avertissement indiquant que le fichier sur disque a changé. Une correction qui « n’a rien changé » est souvent une correction que systemd n’a pas encore relue.

Deux autres commandes sont également utiles. systemctl cat myapp.service affiche l’unité effective, c’est-à-dire le fichier principal et tous les drop-ins situés sous /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory affiche ces valeurs telles que systemd les a analysées. C’est cette configuration qui sera réellement exécutée.

Que signifie status=203/EXEC ?

203/EXEC signifie que systemd a terminé la préparation, a appelé execve(), puis que le kernel a refusé l’exécution. Votre programme n’a exécuté aucune ligne de son propre code. Quatre causes couvrent presque tous les cas.

  1. Le chemin indiqué dans ExecStart= est incorrect ou n’est pas absolu. Vérifiez-le avec ls -l en le comparant à la chaîne exacte du fichier d’unité.
  2. Le fichier n’a pas le bit d’exécution. sudo chmod +x /opt/myapp/run.sh corrige le problème. Un fichier extrait d’une archive ou copié depuis une autre machine perd souvent ce bit.
  3. La ligne shebang est incorrecte. Le kernel lit la première ligne d’un script et lance l’interpréteur qui y est indiqué. #!/usr/bin/env python3 échoue donc si le PATH du service ne contient pas python3. Un fichier enregistré avec des fins de ligne Windows demande un interpréteur nommé /bin/bash\r, qui n’existe pas.
  4. Le fichier n’est pas exécutable par cette machine : architecture incorrecte ou fichier texte sans shebang.

Reproduisez le problème manuellement, avec l’utilisateur du service, avant de modifier quoi que ce soit.

sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -A

file indique l’architecture et affiche « with CRLF line terminators » lorsque les fins de ligne sont à l’origine du problème. cat -A affiche la même information sous la forme d’un ^M final. Supprimez-les avec sed -i 's/\r$//' /opt/myapp/run.sh.

Une précision importante sur cette plage : 200 et plus est une convention, pas une garantie. Votre propre programme peut quitter avec le code 203, et systemd ne peut pas distinguer les deux cas. systemd-analyze exit-status 203 affiche le nom et la classe de n’importe quel code, ce qui facilite la lecture du tableau. Toutefois, si votre application utilise des codes de sortie supérieurs à 199, modifiez-les.

Pourquoi obtenir 217/USER ou 216/GROUP ?

217/USER signifie que le compte indiqué dans User= n’existe pas au moment où le service démarre. 216/GROUP correspond au même échec pour Group= ou SupplementaryGroups=. Vérifiez-le avec une commande pour chaque cas.

getent passwd appuser
getent group appgroup

Chaque commande affiche soit une ligne, soit rien et retourne un code différent de zéro. L’absence de résultat signifie que le nom est inconnu du système. systemd ne peut donc pas l’utiliser et s’arrête avant exec. La correction consiste à créer le compte, et non à définir User=root. Exécuter un service avec un compte système dédié disposant du minimum de privilèges est précisément l’objectif de cette directive.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser

DynamicUser=yes contourne le problème en demandant à systemd d’allouer un compte temporaire à chaque démarrage. Cette solution convient à un service qui ne conserve aucun état. Tout service qui écrit des fichiers doit également utiliser StateDirectory=, car l’ID utilisateur change entre les démarrages et les fichiers stockés dans un chemin standard finissent associés à un compte qui n’existe plus.

Qu’est-ce que 226/NAMESPACE ?

226/NAMESPACE provient des directives de sandboxing. Lorsqu’une unité définit ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= ou une directive similaire, systemd crée un mount namespace privé pour ce service avant d’exécuter le programme. Ici, un namespace est une vue privée du système de fichiers pour un processus. Si un mount de ce plan échoue, le démarrage échoue avec le code 226 et votre programme ne s’exécute jamais.

La cause habituelle est un chemin indiqué dans ReadWritePaths= qui n’existe pas. ProtectSystem=strict monte tout le système de fichiers en lecture seule, et ReadWritePaths= rouvre les chemins nommés en écriture. systemd ne peut pas rouvrir un répertoire qui n’existe pas. Deux corrections conviennent. Laissez systemd créer le répertoire avec StateDirectory=, ce qui crée /var/lib/<name> à chaque démarrage et le donne à l’utilisateur du service, ou préfixez le chemin avec -, ce qui indique à systemd d’ignorer cette entrée lorsque la source est absente. La mauvaise correction consiste à supprimer le hardening, ce qui transforme un problème de cinq minutes en problème permanent.

[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploads

Lorsque vous ne pouvez pas déterminer quelle ligne est responsable, supprimez tout le bloc de hardening, rechargez la configuration, puis démarrez le service. Si le service démarre, remettez les lignes une par une et redémarrez après chacune d’elles. Deux codes voisins de cette famille sont 233/RUNTIME_DIRECTORY et 238/STATE_DIRECTORY. Ils signifient que systemd n’a pas pu créer le répertoire indiqué dans RuntimeDirectory= ou StateDirectory=, ou s’en rendre propriétaire, généralement parce que ce chemin existe déjà et appartient à un autre utilisateur.

Pourquoi 200/CHDIR apparaît-il alors que WorkingDirectory semble correct ?

200/CHDIR signifie que le chdir() vers WorkingDirectory= a échoué. Le répertoire est absent ou l’utilisateur du service ne peut pas y accéder. Pour entrer dans un répertoire, il faut disposer de la permission d’exécution sur ce répertoire et sur chacun de ses répertoires parents. Ainsi, un /home/deploy/app parfaitement lisible reste inaccessible lorsque /home/deploy est en mode 700 et que le service s’exécute sous appuser.

sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myapp

namei -l affiche le propriétaire et le mode de chaque composant du chemin. C’est le moyen le plus rapide de trouver le répertoire qui bloque l’accès au reste du chemin. Écrire WorkingDirectory=-/srv/myapp rend l’absence du répertoire non bloquante. C’est adapté à un programme qui n’a pas besoin de démarrer dans un emplacement précis, mais pas à un programme qui ouvre des fichiers avec un chemin relatif.

Pourquoi le service démarre-t-il puis s’arrête-t-il une seconde plus tard ?

Il n’y a ici aucun code de la série 200, et souvent aucun message d’erreur. L’unité affiche inactive (dead) juste après son démarrage ou alterne entre activating (auto-restart). systemd a correctement construit l’environnement. Le problème vient d’un décalage entre le comportement de votre programme et ce que Type= indique qu’il doit faire.

Type=simple, la valeur par défaut, indique que le programme reste au premier plan. Si vous lui fournissez un daemon qui se détache en arrière-plan et se termine, systemd voit le processus principal se terminer et considère le service comme terminé. La plupart des daemons disposent d’une option pour rester au premier plan, par exemple nginx -g 'daemon off;'.

Type=forking indique que le premier processus se termine dès que son processus enfant est prêt. Si vous lui fournissez un programme au premier plan, la tâche de démarrage attend jusqu’à l’expiration de TimeoutStartSec=, fixée par défaut à 90 secondes. systemd le tue alors et journalise un dépassement de délai.

Type=notify indique que le programme appelle sd_notify() pour signaler qu’il est prêt. Un programme qui ne prend pas cette fonction en charge ne signale rien. Le démarrage expire alors et le journal enregistre le résultat comme un échec de protocole.

Choisissez le type en fonction du comportement réel du programme. Différences entre simple, forking, oneshot et notify est la décision qui permet de résoudre toute cette catégorie de problèmes.

Lorsqu’un service se termine à répétition, systemd cesse les tentatives et indique que la demande de démarrage s’est répétée trop rapidement. L’unité reste alors en échec jusqu’à l’expiration de la fenêtre de limitation ou jusqu’à l’exécution de sudo systemctl reset-failed myapp.service. Augmenter la limite ne fait que masquer le symptôme. Consultez le journal à partir du premier échec plutôt que du dernier, et lisez ce que Restart=on-failure relance réellement avant de modifier ce paramètre.

Pourquoi l’unité est-elle inactive sans afficher la moindre erreur ?

Une unité peut être ignorée au lieu d’être démarrée. Les directives Condition* sont silencieuses par conception : lorsque le contrôle échoue, systemd considère le job comme réussi et n’effectue aucune action. Une unité qui contient ConditionPathExists=/etc/myapp/config.yml ne démarrera jamais tant que ce fichier est absent, sans jamais signaler d’erreur.

systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i condition

ConditionResult=no confirme que l’unité a été ignorée, et le journal indique quel contrôle n’a pas été satisfait. Utilisez plutôt une directive Assert* lorsqu’une condition préalable manquante doit provoquer une erreur explicite. La section Conditions, assertions et ordre de démarrage des unités explique quelle vérification utiliser à quel endroit.

D’autres cas silencieux se trouvent à proximité. Une erreur « could not be found » signifie généralement que le fichier se trouve dans le mauvais répertoire ou que vous n’avez pas rechargé la configuration : les fichiers d’unité que vous créez doivent se trouver dans /etc/systemd/system/. Une unité masquée refuse tout démarrage tant que sudo systemctl unmask myapp.service ne l’a pas démasquée. Enfin, systemctl enable échoue sur une unité qui ne contient aucune section [Install] ; ajoutez-lui donc WantedBy=multi-user.target.

Et si le processus avait été tué au lieu d’échouer ?

code=killed indique une situation différente de code=exited. Un élément externe a arrêté le processus. status=9/KILL désigne généralement l’out of memory (OOM) killer, et le journal indique le processus qu’il a sélectionné. Une limite que vous avez définie vous-même peut produire le même effet dans le cgroup (control group). Vérifiez donc la mémoire disponible sur l’hôte avec free -m et recherchez un MemoryMax= dans l’unité. MemoryMax, CPUQuota et les autres limites de cgroup explique quelle limite tue un processus et laquelle le ralentit seulement.

status=15/TERM juste après une tentative de démarrage signifie généralement que systemd a dépassé le délai de démarrage et arrêté le processus. Vous revenez alors à Type=.

Deux habitudes qui évitent la plupart de ces problèmes

Utilisez partout des chemins absolus. systemd n’exécute pas votre shell de connexion. Il n’y a donc pas de .bashrc, pas de .profile et aucun environnement virtuel activé. $PATH pour un service système est une liste intégrée courte. Elle ne contient ni /opt ni les shims d’un gestionnaire de versions de langage. Écrivez /usr/bin/python3 ou /opt/myapp/venv/bin/python au complet. command -v myapp dans votre shell affiche le chemin à copier. La même règle s’applique à WorkingDirectory=, EnvironmentFile= et à chaque chemin de ReadWritePaths=.

ExecStart= n’est pas un shell. systemd sépare la ligne en mots et appelle directement execve(). Les pipes, les redirections, les jokers, &&, les backticks et ~ n’ont aucune signification : ils sont transmis à votre programme comme des arguments littéraux. ExecStart=/usr/bin/myapp --flag > /tmp/out.log transmet > et /tmp/out.log à myapp, qui se termine alors avec une erreur d’utilisation sans rapport apparent avec un problème systemd. Lorsque vous avez besoin de fonctionnalités du shell, demandez l’exécution d’un shell.

ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'

Pour la sortie seule, ce n’est pas nécessaire. La sortie du service est envoyée au journal par défaut, et StandardOutput=append:/var/log/myapp.log écrit dans un fichier sans faire intervenir de shell.

L’expansion des variables est limitée de la même façon. $MYVAR et ${MYVAR} sont remplacées à partir de Environment= et EnvironmentFile=, et rien d’autre n’est développé. $HOME n’est pas défini pour un service système, sauf si vous le définissez. Un EnvironmentFile= n’est pas non plus un script shell : export n’y a pas sa place, ses règles de guillemets diffèrent de celles de bash, et un fichier manquant provoque une erreur fatale, sauf si vous préfixez le chemin avec -.

Procéder sur un serveur en production

Lisez le code, établissez la cause, modifiez une seule chose, puis redémarrez. Cet ordre est plus important que de connaître chaque valeur, car il évite d’empiler trois modifications spéculatives et de perdre la trace de celle qui a corrigé le problème. La même méthode s’applique aux unités que vous n’avez pas écrites. Un timer qui ne se déclenche jamais correspond à un service qui n’a jamais démarré. Commencez donc par déboguer le service : un timer systemd et le service qu’il déclenche échoue exactement pour les raisons indiquées ci-dessus, le timer masquant la sortie tant que vous ne la demandez pas au journal.

FAQ

Que signifie status=203/EXEC dans systemctl status ?

systemd a configuré tout ce que l’unité demandait, puis l’appel execve() a échoué. Votre programme n’a donc jamais démarré. Vérifiez quatre éléments dans l’ordre : le chemin indiqué dans ExecStart= existe et est absolu, le fichier possède le bit d’exécution, le shebang désigne un interpréteur présent dans le PATH du service, et le fichier utilise des fins de ligne Unix. Pour le dernier point, file affiche « with CRLF line terminators ». Le nom de l’interpréteur devient alors /bin/bash\r, et le kernel refuse d’exécuter le fichier.

Pourquoi mon service démarre-t-il puis s’arrête-t-il immédiatement ?

Le fichier d’unité décrit un comportement que le programme n’a pas. Avec Type=simple, systemd attend que le programme reste au premier plan. Un daemon qui se détache en arrière-plan semble donc terminé dès qu’il se détache. Avec Type=forking, systemd attend que le premier processus se termine. Un programme au premier plan fait alors attendre la tâche de démarrage jusqu’à l’expiration de TimeoutStartSec=. Faites correspondre Type= au comportement du programme. Si le programme propose une option de premier plan, utilisez-la avec la valeur par défaut Type=simple.

Comment voir l’erreur réelle au lieu de la sortie d’état abrégée ?

systemctl status affiche uniquement les dernières lignes du journal et tronque les lignes longues. Exécutez journalctl -u myapp.service -b --no-pager pour obtenir tout ce que l’unité a journalisé pendant ce boot. Ajoutez -n 200 pour élargir la fenêtre, ou envoyez la sortie vers grep. Si l’application écrit son propre fichier journal, consultez-le également. systemd ne capture que ce que le programme envoie vers la sortie standard et la sortie d’erreur standard.

Pourquoi mon unité est-elle inactive sans message d’erreur ?

Le plus souvent, une directive Condition* l’a ignorée. Ces vérifications sont silencieuses : une condition échouée marque la tâche de démarrage comme réussie. Exécutez systemctl show myapp.service -p ConditionResult et recherchez ConditionResult=no. Lisez ensuite la ligne du journal qui indique la vérification concernée. L’autre cause fréquente est une unité masquée. Elle refuse tout démarrage tant que sudo systemctl unmask ne la réactive pas.

Dois-je exécuter daemon-reload après chaque modification d’un fichier d’unité ?

Oui, pour toute modification d’un fichier d’unité ou d’un drop-in. sudo systemctl daemon-reload demande à systemd de relire les fichiers présents sur le disque, puis sudo systemctl restart myapp.service applique ces changements au service en cours d’exécution. Ce n’est pas nécessaire après systemctl edit, qui effectue lui-même le rechargement. Ce n’est pas non plus nécessaire après la modification d’un fichier de configuration appartenant à l’application plutôt qu’à systemd.

#systemd#troubleshooting#journalctl#exit-codes#linux-fundamentals