SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-21

Dependencias y condiciones de systemd explicadas

Diferencia Requires, Wants, After, Before, ExecStartPre y Condition. Aprende qué garantiza cada directiva y cómo depurar una unidad que nunca se inicia.

Requires no significa After

Las dependencias y condiciones de systemd son cuatro mecanismos independientes que muchos archivos de unidad utilizan como si fueran uno solo. Requires= y Wants= determinan qué otras unidades se incorporan. After= y Before= determinan el orden en que se inician las unidades. ExecStartPre= ejecuta una comprobación que puede hacer que falle la unidad. Las familias Condition y Assert determinan si la unidad se ejecuta o no. Cada mecanismo es independiente de los demás. Por tanto, una unidad puede requerir otra y aun así iniciarse exactamente al mismo tiempo.

La última frase explica casi todos los informes de tipo «funciona cuando lo inicio manualmente, pero falla durante el arranque».

[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 incorpora PostgreSQL a la misma transacción de inicio. No espera a que PostgreSQL esté disponible. systemd inicia ambos trabajos en paralelo, por lo que pg_isready se ejecuta mientras PostgreSQL todavía está abriendo su directorio de datos. Sale con el código 2 porque todavía no hay ningún proceso escuchando, y la unidad falla antes de que se alcance ExecStart. Ejecutar sudo systemctl start inventory-api una hora después funciona porque para entonces PostgreSQL ya está iniciado. El archivo de unidad no ha cambiado. Por eso parece correcto.

La solución consiste en añadir una línea.

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

En el mismo punto hay un detalle importante. Una dependencia Requires= que falla sólo impide que la unidad se inicie si también se configura After= para esa dependencia. Sin esta relación de orden, systemd ya ha iniciado la unidad cuando falla la otra, por lo que no queda ningún trabajo que cancelar. Requires= por sí solo no proporciona la protección que muchos esperan. Escriba After= junto a cada Requires= y cada Wants=, salvo que tenga un motivo específico para no hacerlo.

Qué prometen Requires, Wants, Requisite y BindsTo

Todas estas opciones configuran dependencias. Ninguna establece el orden de inicio.

  • Wants=: incorpora la otra unidad. Si falla o no existe, esta unidad se inicia de todos modos. Esto es lo que crea systemctl enable como enlace simbólico dentro de un directorio .wants/.
  • Requires=: incorpora la otra unidad. Si falla y además se ha establecido un orden con After=, esta unidad no se inicia. Si la otra unidad se detiene explícitamente más adelante, esta unidad también se detiene.
  • Requisite=: no incorpora la otra unidad. Si no está activa, esta unidad falla inmediatamente.
  • BindsTo=: funciona como Requires=, y esta unidad también se detiene siempre que la otra se detenga por cualquier motivo, incluso si desaparece hardware.
  • PartOf=: la detención y el reinicio se propagan desde la otra unidad hacia esta. El inicio no se propaga.
  • Conflicts=: al iniciar esta unidad, se detiene la otra.

Para un daemon que se comunica con otro daemon, Wants= y After= suelen ser la pareja adecuada. Requires= acopla sus ciclos de vida: si detiene la base de datos para realizar tareas de mantenimiento, la aplicación también se detiene y no vuelve a iniciarse cuando la base de datos vuelve a estar disponible. Wants= y After= proporcionan el orden de inicio durante el arranque sin ese acoplamiento, y una política de reinicio gestiona el caso en que la dependencia desaparezca más adelante.

También se heredan dependencias que nunca se han escrito. Con DefaultDependencies=yes, que es el valor predeterminado, un servicio normal obtiene Requires=sysinit.target, After=sysinit.target basic.target y Conflicts=shutdown.target automáticamente. Por eso un servicio con una sección [Unit] casi vacía todavía se inicia tarde durante el arranque y se detiene correctamente al apagar el sistema.

After y Before solo ordenan la transacción; no hacen nada más

After= y Before= solo establecen el orden. No expresan ningún requisito. After=redis.service en una unidad que no incluye Redis mediante ninguna otra dependencia no tiene efecto: si redis.service no forma parte de la transacción, no hay nada que esperar y la unidad se inicia de inmediato.

Conviene repetirlo, porque ese es exactamente el patrón del error de network-online.target que se explica más adelante. El orden sólo espera a las unidades que ya se están iniciando en la misma transacción.

El par es simétrico. After=b.service escrito en a.service significa lo mismo que Before=a.service escrito en b.service, así que use una de las dos opciones y colóquela en la unidad que administra. Durante el apagado, el orden se invierte automáticamente, por lo que After=b.service también significa que la unidad se detiene antes que b.service.

After= espera a que se produzca «started», y Type= define qué significa

After= espera hasta que la otra unidad haya terminado de iniciarse. El significado de «terminado de iniciarse» lo decide por completo el Type= de esa unidad.

  • Type=simple: en cuanto systemd haya bifurcado el proceso. Es posible que el programa todavía no haya analizado su configuración, y mucho menos abierto un socket.
  • Type=exec: en cuanto execve() se haya ejecutado correctamente. Es ligeramente más estricto. Aun así, no indica que el servicio esté listo.
  • Type=forking: cuando finaliza el proceso padre original.
  • Type=oneshot: cuando finaliza el proceso. En este caso, «iniciado» significa realmente que el trabajo ha terminado.
  • Type=notify: cuando el servicio envía READY=1 por su socket de notificaciones. Este es el único tipo que informa de una disponibilidad real.

Por tanto, After= sobre un daemon Type=simple es una garantía débil, y esa es la segunda mitad de la condición de carrera del primer ejemplo. Si la unidad de la que depende se distribuye como Type=simple, ordenar el inicio después de ella no significa que esté aceptando conexiones. Hay dos respuestas correctas. Ordene el inicio después de su unidad de socket para que el kernel ponga en cola las conexiones entrantes mientras el daemon todavía se inicia. O haga que su propio servicio reintente y deje que la política de reinicio se encargue de ello. El tipo que usa una unidad se puede consultar en systemctl cat, y conviene leer la configuración Type= y lo que cada valor indica a systemd antes de depender del orden de inicio.

ExecStartPre es una barrera que puede hacer que la unidad falle

ExecStartPre= se ejecuta antes de ExecStart=. Si termina con un código distinto de cero, la activación se cancela y la unidad pasa a failed. ExecStart= nunca se ejecuta. Este es el motivo de muchos fallos de unidades en los que no aparece ningún mensaje del programa real: el programa nunca llegó a iniciarse.

Aspectos importantes:

  • No es un shell. No admite tuberías, redirecciones, comodines ni &&. El primer token debe ser una ruta absoluta. Encierre la línea en /bin/sh -c '...' cuando necesite sintaxis de shell.
  • Un prefijo - hace que un código de salida distinto de cero no sea fatal: ExecStartPre=-/usr/bin/optional-check.
  • Cada ExecStartPre= debe terminar antes de que se ejecute el siguiente. No puede iniciar un proceso de larga duración.
  • Todas las líneas ExecStartPre= comparten TimeoutStartSec= con ExecStart=. Una comprobación previa que espera en un bucle a una base de datos consume el tiempo de espera de inicio. Después, la unidad falla con Result: timeout cuando start operation timed out. Terminating. aparece en el journal.

La línea del fallo identifica el proceso de control, no el proceso principal:

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

Lea atentamente ese nombre simbólico. systemd asigna los códigos de salida pequeños mediante una tabla fija. Por eso 2 siempre muestra INVALIDARGUMENT, independientemente del significado que el programa pretendiera darle. status=203/EXEC es el valor que contiene la información real: systemd no pudo ejecutar el binario porque la ruta es incorrecta o el archivo no es ejecutable.

No use ExecStartPre= para crear directorios. RuntimeDirectory=, StateDirectory=, LogsDirectory= y CacheDirectory= los crean con el propietario y los permisos adecuados, y RuntimeDirectory= se elimina cuando el servicio se detiene. También funcionan correctamente con DynamicUser=, a diferencia de un mkdir escrito manualmente.

Condition omite el error. Assert lo muestra de forma explícita.

Las familias Condition y Assert ejecutan las mismas pruebas. Sólo se diferencian en lo que ocurre cuando una prueba falla.

Un Condition...= fallido omite la unidad. El trabajo de inicio se informa como correcto. La unidad permanece inactive (dead), nada se marca como fallido, no se genera ninguna alerta y el journal registra una línea:

Condition check resulted in Inventory API being skipped.

En systemd 250 y versiones posteriores, systemctl status muestra directamente el motivo:

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

La línea con sangría que aparece debajo identifica la directiva exacta que falló, por ejemplo ConditionPathExists=/etc/inventory/api.conf was not met.

Un Assert...= fallido hace que falle la unidad. El journal indica Assertion failed for Inventory API. y la unidad termina en failed (Result: assert), lo bastante explícito para que la supervisión lo detecte.

Elija entre ambos preguntándose qué significa que falle la prueba. Condition significa «esta unidad no se aplica en esta máquina». Assert significa «esto debe cumplirse y, si no se cumple, hay que avisar a alguien». La mayoría de las unidades necesitan Condition. Use Assert sólo cuando no hacer nada de forma silenciosa sea peor que tener una unidad fallida.

La familia Condition tiene dos errores frecuentes.

Primero, una condición fallida no hace fallar a las unidades que dependen de ella. Si a.service tiene Requires=b.service y b.service se omite por una condición, el trabajo de inicio de b.service sigue considerándose completado, por lo que a.service se inicia normalmente aunque b no esté en ejecución. Una condición sólo protege la unidad en la que está escrita.

Segundo, las condiciones se evalúan cada vez que se inicia la unidad, en el momento en que se ejecuta el trabajo. Una unidad activada por un temporizador de systemd en un VPS puede omitirse cien veces seguidas sin aparecer nunca como fallida. Es la misma clase de operación vacía y silenciosa que un trabajo de cron que se ejecuta pero no hace nada, y se detecta de la misma forma: lea el journal de la unidad en lugar de confiar en su estado de salida.

Estas son las condiciones que conviene conocer en un servidor:

  • ConditionPathExists=/etc/inventory/api.conf y su negación ConditionPathExists=!/etc/inventory/api.conf.
  • ConditionFileNotEmpty= y ConditionDirectoryNotEmpty=, para un archivo de configuración o un directorio de datos que un paquete haya creado pero dejado vacío.
  • ConditionVirtualization=, para que una unidad que necesite una interfaz real del kernel pueda incluir ConditionVirtualization=!container. Compruebe lo que informa el equipo con systemd-detect-virt.
  • ConditionHost=, que compara el nombre de host o el ID de la máquina. Así, un mismo archivo de unidad puede comportarse de forma distinta en dos servidores.
  • ConditionKernelCommandLine= y ConditionKernelVersion=, para unidades vinculadas a un parámetro de arranque o a una versión mínima del kernel.

Una asignación vacía borra la lista. Así es como un drop-in elimina una condición incluida por un paquete:

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

Por qué network.target no significa que la red esté operativa

network.target es un punto de sincronización, no un estado. Durante el arranque, ordenar una unidad después de este destino significa que el software de gestión de red ya se ha iniciado. No significa que una interfaz tenga una dirección ni que exista una ruta a Internet. El destino existe principalmente para la dirección contraria: una unidad ordenada After=network.target se detiene antes de que la red se desconfigure durante el apagado.

network-online.target es el que espera. Está respaldado por un servicio wait-online perteneciente al gestor de red que utilice:

  • systemd-networkd-wait-online.service cuando systemd-networkd gestiona los enlaces, que es el caso normal en un servidor Ubuntu configurado mediante netplan.
  • NetworkManager-wait-online.service con NetworkManager.

Las configuraciones antiguas basadas en ifupdown obtienen el mismo efecto mediante networking.service. En cualquier caso, usar el destino correctamente requiere dos líneas, no una.

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

network-online.target no forma parte de la transacción de arranque predeterminada y ningún componente lo incorpora automáticamente. Si escribe sólo After=, está ordenando una unidad que nunca se ha puesto en cola, por lo que la ordenación no hace nada. Es el no-op descrito antes, en su forma más costosa. La línea Wants= es la que incorpora el destino a la transacción, para que la línea After= tenga algo que esperar.

Lo segundo que debe saber es que «online» lo define la implementación de wait-online, no systemd. systemd-networkd-wait-online devuelve el control cuando los enlaces que gestiona alcanzan un estado configurado. No comprueba que DNS resuelva nombres ni que se pueda acceder a ningún host remoto.

Esta definición provoca un fallo habitual en VPS. Una máquina con una segunda interfaz para una red privada, declarada en netplan pero sin dirección asignada, hace que el servicio de espera continúe esperando hasta agotar el tiempo de espera:

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

El arranque tarda dos minutos más porque el tiempo de espera predeterminado es de 120 segundos. Hay dos soluciones. Marque la interfaz sin uso como optional: true en el archivo de netplan para que networkd deje de esperarla. También puede añadir un drop-in al servicio de espera que indique el enlace que le interesa mediante --interface=, o que pase --any para devolver el control en cuanto haya un enlace operativo.

Mejor aún, evite necesitar el destino. Muchos servicios se ordenan después de network-online.target sólo porque se enlazan a una dirección concreta y fallan durante el arranque con una línea como esta:

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

El kernel rechaza el enlace porque esa dirección aún no está operativa. Establecer net.ipv4.ip_nonlocal_bind=1 permite que un proceso se enlace a una dirección que el sistema todavía no tiene, y una política de reinicio resuelve el resto. Retrasar todo el arranque hasta que la red esté operativa es una herramienta excesiva para un problema que normalmente afecta a un solo socket.

Cómo leer las dependencias reales de systemd en un sistema en ejecución

Nunca deduzca las dependencias sólo a partir del archivo de unidad. Los drop-ins, los enlaces simbólicos de .wants/ y las dependencias predeterminadas implícitas también añaden relaciones que el archivo no muestra.

systemctl cat inventory-api.service

Esto muestra el archivo de unidad y todos los drop-ins, en el orden en que se aplican, con la ruta de origen encima de cada bloque. Ejecútelo primero. Una sobrescritura de cinco líneas en /etc/systemd/system/inventory-api.service.d/ tiene prioridad sobre el archivo proporcionado por el paquete y, de otro modo, es invisible.

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

Esto muestra los valores resueltos, después de aplicar los drop-ins y de que systemd haya añadido sus dependencias implícitas. ConditionResult=no responde directamente a la pregunta «la unidad indicó que terminó correctamente y no hizo nada».

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 sencilla recorre Requires= y Wants= hacia abajo. --reverse muestra qué unidades inician la suya, que es la forma de encontrar el target que la inicia durante el arranque. --after y --before muestran el orden, y ese es el par que debe consultar cuando la pregunta es si algo esperó realmente.

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

El segundo comando intercala dos unidades con marcas de tiempo en milisegundos. Así puede demostrar una condición de carrera de orden en lugar de hacer suposiciones. El fallo de pg_isready aparece antes de que PostgreSQL registre database system is ready to accept connections, y la separación entre ambos se ve directamente en la salida.

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

verify carga la unidad como lo haría systemd e informa de directivas desconocidas, dependencias con unidades inexistentes, ciclos de orden y sintaxis que no puede analizar. No modifica nada en el sistema. critical-chain muestra la cadena de orden que retrasó la unidad, con la hora en que cada paso pasó a estar activo, y sólo funciona con una unidad que se haya iniciado durante el arranque actual.

Después de editar cualquier archivo de unidad, ejecute sudo systemctl daemon-reload. Para cambiar una unidad proporcionada por un paquete, use sudo systemctl edit inventory-api.service, que crea un drop-in automáticamente. Editar el archivo del proveedor en /usr/lib/systemd/system/ funciona hasta que la siguiente actualización del paquete lo sustituye. El mismo mecanismo de drop-in permite añadir límites de memoria y CPU a un servicio sin modificar un archivo propiedad del paquete.

Ciclos de ordenación y la línea que dejan en el journal

Añada la ordenación en ambas direcciones y systemd rompe el ciclo eliminando uno de los trabajos:

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 decide qué trabajo eliminar y puede que no elija el que usted esperaba. El resultado se manifiesta como un servicio que falta después de algunos reinicios y aparece después de otros. Esto es muy difícil de depurar desde fuera. La mayoría de los ciclos se deben a unidades que establecen DefaultDependencies=no y después se ordenan respecto a basic.target de todos modos, o a la adición de Before= a una unidad que ya tenía After= apuntando hacia ella. systemd-analyze verify los detecta sin reiniciar.

La unidad definitiva

[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

Cada línea cumple una función. Wants= incorpora ambas dependencias a la transacción sin vincular a ellas el ciclo de vida de esta unidad. After= gestiona la espera y debe repetir ambos nombres porque la dependencia y el orden son configuraciones independientes. ConditionPathExists= indica que una máquina que tiene el paquete, pero no la configuración, omite la unidad sin generar una alerta. Este es el comportamiento correcto para un servicio controlado por configuración. Type=notify hace que cualquier elemento ordenado después de esta unidad espere a que el servicio esté realmente listo, no a que cree un proceso hijo y termine. Restart=on-failure cubre el caso en que la base de datos deja de estar disponible mucho después del arranque, porque el orden sólo se aplica al primer inicio. La intensidad de esos reintentos se controla con las opciones Restart= y RestartSec=.

Compruébela antes de confiar en ella:

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

Una unidad en buen estado muestra ConditionResult=yes mediante ActiveState=active, y Result=success confirma que no se produjo ningún fallo en la última ejecución. ConditionResult=no junto con ActiveState=inactive indica que la unidad se omitió. La línea del journal que identifica la condición indica qué prueba falló.

FAQ

¿Requires= espera a que se inicie la otra unidad?

No. Requires= y After= son configuraciones independientes. Requires= incorpora la otra unidad a la misma transacción y, después, systemd inicia ambos trabajos en paralelo. Para esperar, añada After= con el nombre de la misma unidad. Hay una segunda razón para añadirlo: una dependencia Requires= que falla sólo impide que se inicie su unidad cuando también se establece After=, porque sin ordenación su unidad ya se ha iniciado cuando falla la otra.

¿Debo ordenar después de network.target o network-online.target?

Durante el arranque, network.target sólo significa que se inició el software de gestión de red. No garantiza que existan direcciones ni rutas. Use network-online.target cuando el servicio necesite una dirección operativa al iniciarse y escriba tanto Wants=network-online.target como After=network-online.target, porque el target no forma parte de la transacción de arranque predeterminada y After= por sí solo espera una unidad que nadie ha puesto en cola. Si el servicio sólo falla porque intenta enlazarse a una IP concreta, net.ipv4.ip_nonlocal_bind=1 con Restart=on-failure es una opción más ligera que retrasar el arranque.

¿Por qué mi unidad informa de que se inició correctamente, pero nunca se ejecuta?

Una prueba Condition...= fallida omite la unidad y comunica que el trabajo de inicio finalizó correctamente, por lo que nunca se marca ningún fallo. Ejecute systemctl show <unit> -p ConditionResult y ConditionResult=no lo confirmará. Después, lea journalctl -b -u <unit> para encontrar la línea Condition check resulted in <description> being skipped. En systemd 250 y versiones posteriores, systemctl status <unit> también indica la directiva exacta que no se cumplió.

¿Cuál es la diferencia entre Condition y Assert?

Ejecutan pruebas idénticas. Si falla Condition, la unidad se omite silenciosamente y el trabajo sigue finalizando correctamente. Si falla Assert, la unidad falla, registra Assertion failed for <description>. y queda en failed (Result: assert). Use Condition para indicar que «esta unidad no se aplica en esta máquina». Esto cubre casi todos los casos reales. Use Assert sólo cuando una condición previa ausente deba ser visible para quien supervise las unidades fallidas.

¿Por qué ExecStartPre falla con status=203/EXEC?

203/EXEC significa que systemd no pudo ejecutar el comando. Las causas habituales son una ruta que no es absoluta, un binario que no existe en esa máquina, un archivo sin el bit de ejecución o un script cuya línea #! apunta a un intérprete inexistente. Los demás códigos pequeños de systemd proceden de una tabla fija, por lo que status=2/INVALIDARGUMENT sólo significa que el comando terminó con el código 2 y no proporciona información sobre los argumentos. Recuerde que ExecStartPre= no se ejecuta mediante un shell, por lo que las tuberías y los patrones glob necesitan /bin/sh -c '...'.

#systemd#units#dependencies#ordering#troubleshooting