SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Systemd no inicia: cómo leer el código de salida

Consulte systemctl status primero: 203/EXEC y 226/NAMESPACE indican fallos distintos, y una unidad puede iniciar bien pero terminar un segundo después.

Por qué no se inicia una unidad de systemd

Una unidad de systemd que no se inicia indica el motivo en un campo. Ejecute systemctl status <unit> y busque code= y status= en la línea que informa del fallo. Un estado en los 200 indica que systemd nunca llegó a ejecutar el programa: falló al crear el entorno solicitado por el archivo de unidad. Un estado inferior a 200 indica que el programa sí se ejecutó y terminó por su cuenta. Por tanto, es probable que el archivo de unidad sea correcto y que el problema esté en la aplicación.

Esta separación define el proceso de diagnóstico. Todo lo siguiente se basa en ella, en el orden en que aparecen los números.

Qué tres comandos responden a la pregunta, en orden

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

systemctl status da el veredicto. Lea primero la línea Loaded:, porque indica el archivo que systemd analizó realmente y si la unidad está habilitada, enmascarada o no se encuentra. Después, lea la línea Active: y el par code= y status= que aparece debajo.

journalctl -u myapp.service -b --no-pager muestra los detalles. -u filtra esa unidad concreta, -b limita la salida al arranque actual para que no lea un fallo de la semana pasada y --no-pager imprime directamente en el terminal, de modo que puede pasarlo a grep. status muestra sólo las últimas líneas del registro y acorta las líneas largas. El journal muestra todo lo que el programa escribió antes de terminar, que normalmente es el error real. Añada -n 100 para consultar más historial o ejecútelo con -f en un segundo terminal mientras reinicia la unidad.

systemd-analyze verify carga un archivo de unidad sin ejecutarlo. Advierte sobre secciones y directivas desconocidas, y señala los comandos de ExecStart= que no puede ejecutar. Esto detecta dos tipos de errores silenciosos: una clave mal escrita, que systemd ignora al cargarla y sobre la que muestra una advertencia que casi nadie lee, y una ruta que no existe.

Después de editar cualquier archivo de unidad, ejecute sudo systemctl daemon-reload. Hasta que lo haga, systemd seguirá usando la copia que cargó antes y systemctl status añadirá una advertencia indicando que el archivo del disco ha cambiado. Una corrección que «no hizo nada» suele ser una corrección que systemd todavía no ha leído.

Hay otros dos comandos importantes. systemctl cat myapp.service muestra la unidad efectiva, es decir, el archivo principal y todos los drop-ins de /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory muestra esos valores tal como systemd los analizó, que es la configuración que se ejecutará realmente.

¿Qué significa status=203/EXEC?

203/EXEC indica que systemd terminó la configuración, llamó a execve() y el kernel rechazó la ejecución. El programa no llegó a ejecutar ninguna línea de su propio código. Cuatro causas cubren casi todos los casos.

  1. La ruta de ExecStart= es incorrecta o no es absoluta. Compruébela con ls -l y compárela con la cadena exacta del archivo de unidad.
  2. El archivo no tiene el bit de ejecución. sudo chmod +x /opt/myapp/run.sh lo corrige. Un archivo extraído de un archivo comprimido o copiado desde otra máquina suele perder ese bit.
  3. La línea shebang está dañada. El kernel lee la primera línea de un script y ejecuta el intérprete indicado allí. Por eso #!/usr/bin/env python3 falla cuando el PATH del servicio no contiene ningún python3, y un archivo guardado con finales de línea de Windows solicita un intérprete llamado /bin/bash\r, que no existe.
  4. El archivo no es algo que esta máquina pueda ejecutar: tiene una arquitectura incorrecta o es un archivo de texto sin ninguna línea shebang.

Reprodúzcalo manualmente, como el usuario del servicio, antes de cambiar nada.

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

file muestra la arquitectura y devuelve "with CRLF line terminators" cuando el problema está en los finales de línea. cat -A muestra lo mismo mediante un ^M final. Elimínelos con sed -i 's/\r$//' /opt/myapp/run.sh.

Hay una salvedad importante sobre el rango: 200 o superior es una convención, no una garantía. Su propio programa puede terminar con el código 203, y systemd no puede distinguir ambos casos. systemd-analyze exit-status 203 muestra el nombre y la clase de cualquier código, lo que ayuda a leer la tabla, pero si la aplicación usa códigos de salida superiores a 199, cámbielos.

¿Por qué aparece 217/USER o 216/GROUP?

217/USER indica que la cuenta especificada en User= no existe cuando se inicia el servicio. 216/GROUP indica el mismo fallo para Group= o SupplementaryGroups=. Confírmelo con un comando para cada caso.

getent passwd appuser
getent group appgroup

Cada comando muestra una línea, o no muestra nada y devuelve un código distinto de cero. Si no muestra nada, el nombre no existe en el sistema. Por eso systemd no puede cambiar a esa cuenta y se detiene antes de ejecutar el proceso. La solución es crear la cuenta, no configurar User=root. Ejecutar cada servicio con una cuenta de sistema dedicada con privilegios mínimos es precisamente el objetivo de esa directiva.

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

DynamicUser=yes evita el problema porque hace que systemd asigne una cuenta temporal en cada inicio. Es adecuado para un servicio que no conserva estado. Todo lo que escriba archivos necesita también StateDirectory=, porque el ID de usuario cambia entre los inicios y los archivos de una ruta normal terminan perteneciendo a una cuenta que ya no existe.

¿Qué es 226/NAMESPACE?

226/NAMESPACE procede de las directivas de aislamiento. Cuando una unidad define ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= o algo similar, systemd crea un espacio de nombres de montajes privado para ese servicio antes de ejecutar el programa. Aquí, un espacio de nombres es una vista privada del sistema de archivos para un proceso. Si falla cualquier montaje de ese plan, el arranque termina con 226 y el programa nunca se ejecuta.

La causa habitual es que no existe una ruta incluida en ReadWritePaths=. ProtectSystem=strict monta todo el sistema de archivos como de solo lectura y ReadWritePaths= vuelve a abrir las rutas indicadas con permisos de escritura. systemd no puede volver a abrir un directorio que no existe. Hay dos soluciones adecuadas. Permita que systemd cree el directorio con StateDirectory=, que crea /var/lib/<name> en cada arranque y se lo asigna al usuario del servicio, o anteponga - a la ruta para indicar a systemd que ignore esa entrada si falta el origen. La solución incorrecta es eliminar el aislamiento, porque convierte un problema de cinco minutos en uno permanente.

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

Si no puede determinar qué línea es la responsable, elimine todo el bloque de aislamiento, recargue la configuración e inicie el servicio. Si el servicio arranca, vuelva a añadir las líneas una por una y reinicie después de cada cambio. Dos directivas relacionadas son 233/RUNTIME_DIRECTORY y 238/STATE_DIRECTORY. Indican que systemd no pudo crear o asumir la propiedad del directorio especificado en RuntimeDirectory= o StateDirectory=, normalmente porque esa ruta ya existe y pertenece a otro usuario.

¿Por qué aparece 200/CHDIR cuando WorkingDirectory parece correcto?

200/CHDIR indica que chdir() en WorkingDirectory= falló. El directorio no existe o el usuario del servicio no puede acceder a él. Para acceder a un directorio se necesita permiso de ejecución en ese directorio y en todos sus directorios padre. Por eso, un /home/deploy/app perfectamente legible resulta inaccesible cuando /home/deploy tiene el modo 700 y el servicio se ejecuta como appuser.

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

namei -l muestra el propietario y el modo de cada componente de la ruta. Es la forma más rápida de encontrar el directorio que bloquea el acceso al resto. Escribir WorkingDirectory=-/srv/myapp hace que la ausencia del directorio no sea fatal. Esto es adecuado para un programa al que no le importa desde dónde se inicia, pero no para uno que abre archivos mediante rutas relativas.

¿Por qué el servicio se inicia y se detiene un segundo después?

Aquí no hay ningún código de la serie 200 y, a menudo, tampoco aparece ningún texto de error. La unidad muestra inactive (dead) justo después de iniciarse o pasa repetidamente por activating (auto-restart). systemd creó correctamente el entorno. La discrepancia está entre lo que hace el programa y lo que Type= indica que debería hacer.

Type=simple, el valor predeterminado, indica que el programa permanece en primer plano. Si se le proporciona un daemon que se bifurca en segundo plano y termina, systemd considera que el proceso principal finalizó y da el servicio por completado. La mayoría de los daemons tienen una opción para permanecer en primer plano, como nginx -g 'daemon off;'.

Type=forking indica que el primer proceso termina cuando su proceso hijo está listo. Si se proporciona un programa en primer plano, la tarea de inicio espera hasta que se agota TimeoutStartSec=, que es de 90 segundos de forma predeterminada. Después, systemd lo detiene y registra un tiempo de espera agotado.

Type=notify indica que el programa llama a sd_notify() para anunciar que está listo. Un programa sin esa compatibilidad no anuncia nada. Por tanto, el inicio agota el tiempo de espera y el journal registra el resultado como un fallo de protocolo.

Elija el tipo según el comportamiento real del programa. Diferencias entre simple, forking, oneshot y notify es la decisión que resuelve toda esta clase de fallos.

Cuando un servicio termina una y otra vez, systemd deja de intentarlo e indica que la solicitud de inicio se repitió demasiado rápido. La unidad permanece en estado failed hasta que transcurre la ventana del límite de frecuencia o se ejecuta sudo systemctl reset-failed myapp.service. Aumentar el límite sólo oculta el síntoma. Lea el journal desde el primer fallo, no desde el último, y consulte qué reintenta realmente Restart=on-failure antes de cambiarlo.

¿Por qué la unidad está inactiva sin mostrar ningún error?

Una unidad se puede omitir en lugar de iniciarse. Las directivas Condition* son silenciosas por diseño: cuando la comprobación falla, systemd marca el trabajo como correcto y no hace nada. Una unidad que contiene ConditionPathExists=/etc/myapp/config.yml nunca se iniciará mientras falte ese archivo, pero tampoco informará de ningún error.

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

ConditionResult=no confirma que la unidad se omitió, y el journal indica qué comprobación no se cumplió. Use una directiva Assert* cuando la falta de un requisito previo deba producir un error explícito. Condiciones, aserciones y orden de las unidades explica qué comprobación corresponde en cada caso.

Hay otros casos silenciosos relacionados. Un error «could not be found» suele indicar que el archivo está en el directorio incorrecto o que todavía no ha recargado la configuración: los archivos de unidad que cree deben estar en /etc/systemd/system/. Una unidad enmascarada rechaza cualquier inicio hasta que sudo systemctl unmask myapp.service la desbloquee. Además, systemctl enable falla si la unidad no tiene una sección [Install], así que debe añadirle WantedBy=multi-user.target.

¿Qué ocurre si el proceso terminó porque lo mataron y no porque fallara?

code=killed es un caso distinto de code=exited. Algo externo al proceso lo terminó. status=9/KILL apunta al out of memory (OOM) killer, y el journal indica qué proceso seleccionó. Un límite configurado por usted produce el mismo resultado dentro del cgroup (control group), así que compruebe la memoria libre del host con free -m y revise si la unidad tiene configurado un MemoryMax=. MemoryMax, CPUQuota y los demás límites de cgroup explica qué límite mata un proceso y cuál sólo lo ralentiza.

status=15/TERM justo después de intentar iniciar un servicio suele indicar que systemd agotó el tiempo de espera del arranque y terminó el proceso, por lo que debe volver a Type=.

Dos hábitos que evitan la mayoría de estos fallos

Use rutas absolutas en todas partes. systemd no ejecuta su shell de inicio de sesión, por lo que no existe .bashrc, no existe .profile y no hay ningún entorno virtual activado. $PATH para un servicio del sistema es una lista integrada y breve que no incluirá /opt ni los shims de un gestor de versiones de lenguajes. Escriba /usr/bin/python3 o /opt/myapp/venv/bin/python completos. command -v myapp en su shell muestra la ruta que debe copiar. La misma regla se aplica a WorkingDirectory=, EnvironmentFile= y a todas las rutas de ReadWritePaths=.

ExecStart= no es un shell. systemd divide la línea en palabras y ejecuta directamente execve(). Las tuberías, redirecciones, comodines, &&, las comillas invertidas y ~ no tienen ningún significado: llegan al programa como argumentos literales. ExecStart=/usr/bin/myapp --flag > /tmp/out.log entrega > y /tmp/out.log a myapp, que sale con un error de uso que no se parece en nada a un problema de systemd. Cuando necesite funciones del shell, invoque un shell.

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

Para obtener sólo la salida no necesita hacer eso. La salida del servicio se envía al journal de forma predeterminada, y StandardOutput=append:/var/log/myapp.log escribe en un archivo sin utilizar ningún shell.

La expansión de variables está limitada de la misma forma. $MYVAR y ${MYVAR} se sustituyen a partir de Environment= y EnvironmentFile=, y no se expande nada más. $HOME no está definido para un servicio del sistema a menos que lo establezca. Un EnvironmentFile= tampoco es un script de shell: export no debe aparecer en él, sus reglas de comillas son distintas de las de bash y la ausencia de un archivo provoca un error fatal, a menos que anteponga - a la ruta.

Resolverlo en un servidor en producción

Lea el código, demuestre la causa, cambie una sola cosa y reinicie. Ese orden importa más que conocer cada número, porque evita acumular tres cambios especulativos y perder el control de cuál de ellos ayudó. El mismo procedimiento sirve para las unidades que no escribió usted. Un temporizador que nunca se ejecuta depende de un servicio que nunca se inició, así que depure primero el servicio: un temporizador de systemd y el servicio que activa falla exactamente de las formas anteriores, y el temporizador oculta la salida hasta que la solicita en el journal.

FAQ

¿Qué significa status=203/EXEC en systemctl status?

systemd configuró todo lo que la unidad solicitaba, pero la llamada execve() falló, por lo que el programa nunca se inició. Compruebe estos cuatro puntos en orden: que la ruta de ExecStart= exista y sea absoluta, que el archivo tenga el permiso de ejecución, que el shebang indique un intérprete existente en el PATH del servicio y que el archivo use finales de línea Unix. file muestra "with CRLF line terminators" en el último caso. Esto convierte el nombre del intérprete en /bin/bash\r y hace que el kernel lo rechace.

¿Por qué mi servicio se inicia y se detiene de inmediato?

El archivo de unidad define un comportamiento que el programa no tiene. Con Type=simple, systemd espera que el programa permanezca en primer plano. Por tanto, un daemon que se bifurca en segundo plano parece haber terminado en cuanto crea el proceso secundario. Con Type=forking, systemd espera a que termine el primer proceso. Por tanto, un programa en primer plano hace que el trabajo de inicio quede bloqueado hasta que se agote TimeoutStartSec=. Ajuste Type= al comportamiento del programa. Si el programa ofrece una opción para ejecutarse en primer plano, use esa opción con el valor predeterminado Type=simple.

¿Cómo puedo ver el error real en lugar de la salida de estado resumida?

systemctl status sólo muestra las últimas líneas del journal y acorta las líneas largas. Ejecute journalctl -u myapp.service -b --no-pager para obtener todo lo que la unidad registró durante este arranque, añada -n 200 para ampliar el intervalo o redirija la salida mediante una tubería a grep. Si la aplicación escribe su propio archivo de registro, léalo también, porque systemd sólo captura lo que el programa envía a la salida estándar y al error estándar.

¿Por qué mi unidad está inactiva sin mostrar ningún mensaje de error?

Lo más habitual es que una directiva Condition* la haya omitido. Estas comprobaciones son silenciosas: si una condición falla, el trabajo de inicio se marca como correcto. Ejecute systemctl show myapp.service -p ConditionResult y busque ConditionResult=no. Después, lea la línea del journal que identifica la comprobación. Otra causa frecuente es que la unidad esté enmascarada. Una unidad enmascarada rechaza cualquier inicio hasta que sudo systemctl unmask quite la máscara.

¿Necesito ejecutar daemon-reload después de cada cambio en un archivo de unidad?

Sí, cuando edite un archivo de unidad o un drop-in. sudo systemctl daemon-reload hace que systemd vuelva a leer los archivos del disco. Después, sudo systemctl restart myapp.service aplica los cambios al servicio en ejecución. No es necesario ejecutarlo después de systemctl edit, que vuelve a cargar los archivos automáticamente. Tampoco es necesario después de cambiar un archivo de configuración de la aplicación que no pertenezca a systemd.

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