Reinicias una máquina que gestionas en producción y systemd te da la frase menos útil en la historia de Linux:
“Failed to start …”. Sin contexto. Sin pista de qué capa falló. Solo un nombre de unidad triste y una marca de tiempo que ya quedó atrás.
Este es el caso n.º 2: no es una demo de juguete ni un “simple reinstala y listo”. Es el flujo repetible que uso cuando un servicio
falla en Ubuntu 24.04 y necesito una respuesta rápida: qué se rompió, dónde se rompió y qué decisión tomar a continuación.
La velocidad importa, pero la corrección importa más—porque el segundo reinicio es cuando comienza el verdadero outage.
Guía de diagnóstico rápido (primeros 5 minutos)
Cuando una unidad falla, no estás “depurando un servicio”. Estás depurando una transacción que systemd intentó ejecutar:
dependencias, orden, entorno, privilegios, sistema de archivos, red y el binario en sí.
El camino más rápido es dejar de adivinar y forzar a systemd a decirte en qué fase falló.
1) Confirma la unidad exacta que falla y el modo de fallo
- Obtén el nombre de la unidad, no “el nombre de la app”.
- Obtén el resultado y el paso (EXEC, SIGNAL, TIMEOUT, etc.).
- Obtén los últimos registros solo para esa unidad.
2) Identifica la capa que está creando el cuello de botella
El cuello de botella casi siempre es una de estas:
ExecStart no puede ejecutarse (binario faltante, permisos, SELinux/AppArmor),
el proceso arranca pero sale (configuración, puertos, secretos),
nunca llega a “listo” (Type=notify, comprobación de readiness, PIDFile),
o espera por dependencias (network-online, mount, BD, almacenamiento remoto).
3) Decide si necesitas una solución segura para reinicio o un bypass de emergencia
Hay dos tipos de arreglos:
correctos (override de unidad, corrección de dependencias, arreglo de configuración),
y de triage (enmascaramiento temporal, dependencia reducida, arranque manual).
Si el host está atascado en el arranque, el bypass de emergencia es legítimo—pero documéntalo y elimínalo luego.
4) En caso de duda, sigue la cadena, no el síntoma
Un servicio de aplicación fallido a menudo no está roto. Está esperando un montaje, DNS o una ruta de red que nunca llega.
Así que trátalo como un sistema distribuido: encuentra la primera falla en la cadena.
Una cita que aún uso en postmortems, de W. Edwards Deming: “Un mal sistema vencerá a una buena persona siempre.”
Si estás viendo “Failed to start”, asume que el sistema (dependencias, orden, entorno) es culpable hasta que se demuestre lo contrario.
Nueve hechos que cambian cómo depuras systemd
- systemd reemplazó a Upstart en Ubuntu hace años, pero el gran cambio no fue un “nuevo init”—fue la programación por grafo de dependencias. Depurar ahora es recorrer grafos.
- journald no es “solo logs”; guarda campos estructurados (unit, PID, cgroup, ruta del ejecutable). Puedes cortar los registros de forma quirúrgica sin enredos de grep.
- “Failed to start” es un resumen de UI, no una causa raíz. La causa real suele estar en un evento más pequeño: “failed at step EXEC” o “start request repeated too quickly”.
- Las unidades tienen dos relaciones ortogonales: orden (
After=) y requerimiento (Requires=/Wants=). Muchos outages ocurren porque la gente las confunde. - Los targets son puntos de sincronización, no “niveles de ejecución con otro nombre”. Depurar problemas de arranque a menudo significa entender qué target trajo la unidad que falla.
- Type=notify es común en demonios modernos. Si el servicio no envía readiness, systemd puede declarar un timeout aunque el proceso esté vivo.
- StartLimitBurst/Interval son disyuntores. Un servicio que hace flapping puede “fallar” aunque el último intento hubiera tenido éxito.
- Los drop-in overrides son de primera clase. Los vendedores entregan unidades; los operadores las sobreescriben. Editar archivos en
/lib/systemd/systemes cómo te buscas problemas para el futuro. - systemd-run y unidades transitorias existen. Cuando necesites reproducir problemas de entorno, una unidad transitoria puede replicar restricciones de cgroup y sandbox mejor que tu shell.
Broma #1: systemd no te odia personalmente. Odia a todos por igual—luego lo escribe en el journal con precisión de milisegundos.
Modelo mental: de dónde viene realmente “Failed to start”
El trabajo de systemd es tomar una unidad, calcular una transacción de dependencias, ejecutarla y seguir su estado.
La falla que ves es el final de una cadena de transiciones de estado, y quieres la primera divergencia significativa.
Qué significa “start” (en términos de systemd)
“Iniciar” un servicio incluye:
- Cargar la unidad (parseo del archivo de unidad, drop-ins, salida de generadores)
- Resolver dependencias (qué debe existir antes de que esta unidad pueda ejecutarse)
- Ejecutar (ExecStartPre, ExecStart, posiblemente varios procesos)
- Readiness (Type=simple es inmediato; Type=notify requiere una señal; Type=forking necesita un PID)
- Monitoreo (políticas Restart=, watchdogs, fallos)
Clases comunes de fallo (úsalas como cubos)
- La unidad no puede ejecutar: binario faltante, permisos incorrectos, usuario equivocado, intérprete faltante, directorio de trabajo inválido.
- El proceso sale con código no cero: errores de parseo de config, puerto ocupado, dependencia inalcanzable.
- Timeout: readiness no reportada, bloqueo en red, bloqueo en montaje, disco lento, falta de entropía (raro hoy).
- Bloqueo por dependencias: orden incorrecto, After= equivocada, bucle de dependencias.
- Política bloquea: denegaciones AppArmor, sandboxing de systemd (ProtectSystem, PrivateTmp), restricciones de capacidades.
- Limitación por tasa: StartLimitHit, bucles de reinicio.
Tu trabajo en triage es mapear “Failed to start” a uno de estos cubos en menos de cinco minutos.
Todo lo demás es ingeniería, no suposición.
Tareas prácticas de triage (comandos + significado + decisiones)
Abajo están las tareas que realmente ejecuto. Cada una incluye: el comando, qué significa la salida y la decisión que tomas.
Asumo una unidad que falla llamada acme-api.service. Sustituye la tuya.
Tarea 1: Confirma el estado, resultado y el último resumen de error
cr0x@server:~$ systemctl status acme-api.service --no-pager
● acme-api.service - Acme API
Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
Active: failed (Result: timeout) since Mon 2025-12-30 10:12:48 UTC; 32s ago
Duration: 1min 30.012s
Process: 1842 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=killed, signal=TERM)
Main PID: 1842 (code=killed, signal=TERM)
CPU: 1.012s
Dec 30 10:11:18 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: acme-api.service: Failed with result 'timeout'.
Dec 30 10:12:48 server systemd[1]: Failed to start acme-api.service - Acme API.
Significado: systemd mató el proceso tras un timeout de inicio. Eso no significa “se estrelló”. Puede estar ejecutándose bien pero nunca se volvió “listo” según systemd.
Decisión: Siguiente: averiguar si es un problema de readiness/Type, o si está bloqueado esperando algo (red, montaje, BD).
Tarea 2: Extrae registros acotados por unidad (y deja de hacer scroll)
cr0x@server:~$ journalctl -u acme-api.service -b --no-pager -n 200
Dec 30 10:11:18 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 10:11:19 server acme-api[1842]: loading config from /etc/acme/api.yaml
Dec 30 10:11:19 server acme-api[1842]: connecting to postgres at 10.20.0.15:5432
Dec 30 10:11:49 server acme-api[1842]: still waiting for postgres...
Dec 30 10:12:19 server acme-api[1842]: still waiting for postgres...
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: acme-api.service: Failed with result 'timeout'.
Significado: la app está bloqueada en Postgres, y el timeout de systemd es el mensajero.
Decisión: Investiga la alcanzabilidad de la red/DNS/rutas, y verifica la cadena de dependencias de la unidad (¿necesitamos After=network-online.target? ¿lo queremos?).
Tarea 3: Lee el archivo de unidad que systemd está usando realmente
cr0x@server:~$ systemctl cat acme-api.service
# /etc/systemd/system/acme-api.service
[Unit]
Description=Acme API
After=network.target
Wants=network.target
[Service]
Type=notify
ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml
User=acme
Group=acme
Restart=on-failure
TimeoutStartSec=90
[Install]
WantedBy=multi-user.target
Significado: es Type=notify, así que systemd espera readiness. Si el demonio no la envía, systemd hará timeout aunque el proceso esté “bien”.
Además, solo espera network.target, que no es “la red está arriba”.
Decisión: Confirma si el binario soporta sd_notify. Si no, cambia a Type=simple o arregla el demonio. También evalúa si network-online.target es apropiado (a menudo no lo es).
Tarea 4: Comprueba si systemd está esperando por dependencias en vez de por tu servicio
cr0x@server:~$ systemctl list-dependencies --reverse acme-api.service
acme-api.service
● multi-user.target
● graphical.target
Significado: no lo está tirando algo inesperado. Pero esto no muestra retrasos de orden; muestra quién lo quiere.
Decisión: Usa critical chain para ver qué bloqueó el arranque e inspecciona unidades de network-online/mount si están presentes.
Tarea 5: Usa critical chain para encontrar la primera unidad lenta o rota
cr0x@server:~$ systemd-analyze critical-chain acme-api.service
acme-api.service +1min 30.012s
└─network.target @8.412s
└─systemd-networkd.service @6.901s +1.201s
└─systemd-udevd.service @3.112s +3.654s
└─systemd-tmpfiles-setup-dev-early.service @2.811s +201ms
└─kmod-static-nodes.service @2.603s +155ms
Significado: la cadena indica que tu servicio pasó 90 segundos “iniciando”, no que la red fuera lenta.
Esto apunta de nuevo a esperar readiness, no a orden.
Decisión: Confirma si systemd alguna vez recibió READY=1, o si la app está solo bloqueada esperando Postgres.
Tarea 6: Confirma si el proceso estaba vivo durante el “timeout”
cr0x@server:~$ systemctl show acme-api.service -p MainPID -p ExecMainStatus -p ExecMainCode -p TimeoutStartUSec -p Type
MainPID=0
ExecMainStatus=0
ExecMainCode=0
TimeoutStartUSec=1min 30s
Type=notify
Significado: MainPID es 0 ahora porque está muerto (systemd lo mató). La línea clave es Type=notify y un timeout finito.
Decisión: O el demonio nunca notificó readiness o nunca alcanzó readiness porque esperó Postgres. Arregla la dependencia o cambia el comportamiento de inicio.
Tarea 7: Revisa StartLimitHit (el “falló porque falló”)
cr0x@server:~$ systemctl status acme-api.service --no-pager | sed -n '1,18p'
● acme-api.service - Acme API
Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
Active: failed (Result: start-limit-hit) since Mon 2025-12-30 10:13:30 UTC; 5s ago
Process: 1912 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=exited, status=1/FAILURE)
Significado: systemd dejó de intentar porque se reinició con demasiada frecuencia.
Decisión: Reinicia el contador de fallos solo después de que hayas cambiado algo significativo; de lo contrario solo aceleras un crash loop.
Tarea 8: Reinicia un start-limit y reintenta intencionalmente
cr0x@server:~$ sudo systemctl reset-failed acme-api.service
cr0x@server:~$ sudo systemctl start acme-api.service
cr0x@server:~$ systemctl status acme-api.service --no-pager -n 20
● acme-api.service - Acme API
Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
Active: activating (start) since Mon 2025-12-30 10:14:01 UTC; 3s ago
Significado: está en activación otra vez—ahora observa los logs.
Decisión: Si se repite, detente y arregla la causa raíz (config, dependencia, readiness), no sigas insistiendo en start.
Tarea 9: Valida el archivo de unidad por trampas obvias
cr0x@server:~$ systemd-analyze verify /etc/systemd/system/acme-api.service
/etc/systemd/system/acme-api.service:6: Unknown lvalue 'Wants' in section 'Unit'
Significado: En la vida real, pasan errores tipográficos. Aquí marcó una clave inválida (ejemplo). Systemd puede ignorar lo que pensabas que era crítico.
Decisión: Corrige la sintaxis de la unidad; recarga el daemon; reintenta. Si tienes suerte, acabas de encontrar el outage.
Tarea 10: Comprueba si una dependencia está fallando (montajes y almacenamiento son reincidentes)
cr0x@server:~$ systemctl --failed --no-pager
UNIT LOAD ACTIVE SUB DESCRIPTION
● mnt-data.mount loaded failed failed /mnt/data
● acme-api.service loaded failed failed Acme API
LOAD = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state.
SUB = The low-level unit activation state.
Significado: Si el almacenamiento no se montó, tu app puede ser daño colateral.
Decisión: Arregla el montaje primero. Iniciar la app antes de que exista su ruta de datos es cómo obtienes pérdida de datos disfrazada de “disponibilidad”.
Tarea 11: Inspecciona rápidamente una unidad de montaje fallida
cr0x@server:~$ systemctl status mnt-data.mount --no-pager -n 80
● mnt-data.mount - /mnt/data
Loaded: loaded (/proc/self/mountinfo; generated)
Active: failed (Result: exit-code) since Mon 2025-12-30 10:10:02 UTC; 4min ago
Where: /mnt/data
What: UUID=aa1b2c3d-4e5f-6789-a012-b345c678d901
Process: 1222 ExecMount=/usr/bin/mount UUID=aa1b2c3d-4e5f-6789-a012-b345c678d901 /mnt/data (code=exited, status=32)
Status: "Mounting failed."
Dec 30 10:10:02 server mount[1222]: mount: /mnt/data: wrong fs type, bad option, bad superblock on /dev/sdb1, missing codepage or helper program, or other error.
Dec 30 10:10:02 server systemd[1]: mnt-data.mount: Mount process exited, code=exited, status=32/n/a
Significado: fallo clásico de montaje. Podría ser UUID incorrecto, corrupción del FS, módulo del kernel faltante o un disco cambiado.
Decisión: Confirma el dispositivo de bloque, ejecuta blkid, revisa dmesg y solo entonces usa herramientas de reparación de sistemas de archivos.
Tarea 12: Confirma si AppArmor bloqueó el servicio (Ubuntu adora AppArmor)
cr0x@server:~$ journalctl -k -b --no-pager | grep -i apparmor | tail -n 5
Dec 30 10:11:19 server kernel: audit: type=1400 audit(1735553479.112:88): apparmor="DENIED" operation="open" class="file" profile="/usr/local/bin/acme-api" name="/etc/acme/secret.key" pid=1842 comm="acme-api" requested_mask="r" denied_mask="r" fsuid=1001 ouid=0
Significado: Tu servicio podría estar bien; la política no lo está.
Decisión: Actualiza el perfil de AppArmor (o deja de confinar este binario si no puedes mantenerlo). No pongas permisos 777 a secretos “solo para probar”.
Tarea 13: Reproduce la ejecución bajo restricciones parecidas a systemd (unidad transitoria)
cr0x@server:~$ sudo systemd-run --unit=acme-api-debug --property=User=acme --property=Group=acme /usr/local/bin/acme-api --config /etc/acme/api.yaml
Running as unit: acme-api-debug.service
cr0x@server:~$ systemctl status acme-api-debug.service --no-pager -n 30
● acme-api-debug.service - /usr/local/bin/acme-api --config /etc/acme/api.yaml
Loaded: loaded (/run/systemd/transient/acme-api-debug.service; transient)
Active: failed (Result: exit-code) since Mon 2025-12-30 10:15:22 UTC; 2s ago
Process: 2044 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=exited, status=1/FAILURE)
Significado: Esto aísla “funciona en mi shell” de “funciona bajo systemd user/cgroup”.
Decisión: Lee los logs de la unidad transitoria. Si falla igual, probablemente sea config/dependencia, no cableado de la unidad.
Tarea 14: Muestra la interpretación exacta del código de salida
cr0x@server:~$ systemctl show acme-api.service -p ExecMainStatus -p ExecMainCode -p Result
ExecMainStatus=1
ExecMainCode=exited
Result=exit-code
Significado: fallo por código de salida, no timeout, no señal, no watchdog.
Decisión: Concéntrate en stderr de la aplicación y la configuración. No pierdas tiempo en dependencias a menos que los logs apunten allí.
Tarea 15: Comprueba si sockets/puertos fueron el verdadero problema
cr0x@server:~$ ss -ltnp | grep -E ':8080\b'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("nginx",pid=912,fd=12))
Significado: Algo más posee el puerto. Muchos servicios lo registran, pero a veces no antes de salir.
Decisión: Arregla el conflicto de puertos (cambia config, para el otro servicio o usa activación por socket correctamente).
Tarea 16: Cuando el arranque está involucrado, verifica también el arranque previo
cr0x@server:~$ journalctl -u acme-api.service -b -1 --no-pager -n 80
Dec 30 09:03:12 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 09:03:13 server acme-api[701]: connecting to postgres at 10.20.0.15:5432
Dec 30 09:03:14 server acme-api[701]: ready
Dec 30 09:03:14 server systemd[1]: Started acme-api.service - Acme API.
Significado: Ayer funcionó. Eso es una pista: algo cambió (red, secretos, montajes, política, dependencia remota).
Decisión: Busca cambios entre arranques: actualizaciones de paquetes, despliegue de configuración, DNS, firewall, rutas, cambios de almacenamiento.
Caso #2 recorrido: cadena de dependencias + timeout + una comprobación “verde” engañosa
Aquí está la situación que aparece en flotas reales: un servicio falla al iniciar tras un reinicio rutinario, y el error parece local.
No lo es. Es un problema de dependencias/orden que se disfraza de timeout.
La configuración
Tienes un servicio API que lee configuración y luego se conecta a Postgres. La unidad es Type=notify.
Está configurada con After=network.target y Wants=network.target.
En Ubuntu 24.04, la red la gestiona systemd-networkd o NetworkManager dependiendo de la imagen.
La falla empieza así:
cr0x@server:~$ systemctl status acme-api.service --no-pager -n 30
● acme-api.service - Acme API
Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
Active: failed (Result: timeout) since Mon 2025-12-30 10:12:48 UTC; 7min ago
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: Failed to start acme-api.service - Acme API.
La gente a menudo salta directamente a “aumentar TimeoutStartSec.” A veces eso es correcto. A menudo es por pereza.
Primera pregunta: ¿por qué tardó tanto?
Paso 1: El servicio no está “caído”, está “no listo”
El journal muestra que espera por Postgres. Esa es la primera pista: el proceso arrancó y está haciendo trabajo.
La segunda pista es Type=notify. Eso significa que systemd espera readiness, y el timeout es el límite de tiempo en reloj real.
La comprobación más simple: ¿podemos alcanzar Postgres?
cr0x@server:~$ ping -c 2 10.20.0.15
PING 10.20.0.15 (10.20.0.15) 56(84) bytes of data.
--- 10.20.0.15 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1003ms
Significado: Este host no puede alcanzar la IP de BD. Eso no es un bug de la app.
Decisión: Depura routing, VLANs, firewall o el estado del servicio de red. No toques la app aún.
Paso 2: La mentira de “la red está arriba” (network.target)
network.target generalmente significa “el stack de gestión de red está corriendo”, no “tienes rutas y conectividad”.
Si quieres “IP configurada”, normalmente te importa network-online.target—pero trátalo como salsa picante:
un poco ayuda; demasiado arruina la cena.
Comprueba qué provee readiness online:
cr0x@server:~$ systemctl status systemd-networkd-wait-online.service --no-pager -n 40
● systemd-networkd-wait-online.service - Wait for Network to be Configured
Loaded: loaded (/usr/lib/systemd/system/systemd-networkd-wait-online.service; enabled; preset: enabled)
Active: failed (Result: timeout) since Mon 2025-12-30 10:10:55 UTC; 9min ago
Process: 876 ExecStart=/usr/lib/systemd/systemd-networkd-wait-online (code=exited, status=1/FAILURE)
Dec 30 10:09:25 server systemd[1]: Starting systemd-networkd-wait-online.service - Wait for Network to be Configured...
Dec 30 10:10:55 server systemd[1]: systemd-networkd-wait-online.service: start operation timed out. Terminating.
Dec 30 10:10:55 server systemd[1]: systemd-networkd-wait-online.service: Failed with result 'timeout'.
Significado: la red no se volvió “online” dentro del timeout de espera. Tu API nunca tuvo oportunidad si necesita la BD.
Decisión: No “arregles” la API. Arregla por qué el host no alcanzó network-online. Usualmente: netplan incorrecto, VLAN faltante, enlace caído, fallo DHCP o interfaz renombrada.
Paso 3: Encuentra la falla real de red (netplan y estado de interfaz)
cr0x@server:~$ ip -br link
lo UNKNOWN 00:00:00:00:00:00
enp0s31f6 DOWN 3c:52:82:aa:bb:cc
Significado: la interfaz está down. Eso es enlace físico, driver o problema de switch.
Decisión: Si es una VM, revisa la vNIC del hipervisor. Si es física, revisa cableado/puerto del switch y dmesg por problemas del driver.
cr0x@server:~$ journalctl -u systemd-networkd -b --no-pager -n 120
Dec 30 10:09:03 server systemd-networkd[612]: enp0s31f6: Link DOWN
Dec 30 10:09:04 server systemd-networkd[612]: enp0s31f6: Lost carrier
Dec 30 10:09:05 server systemd-networkd[612]: enp0s31f6: DHCPv4 client: No carrier
Significado: sin carrier; DHCP nunca corrió. El “Failed to start” es inocente—tu NIC no lo está.
Decisión: Restaura el enlace. Si el enlace está ausente intencionalmente (red aislada), entonces el diseño del servicio está mal: no debería bloquear el arranque.
Paso 4: La comprobación “verde” engañosa
En muchas organizaciones, alguien ejecuta una comprobación de salud que dice “red OK” porque pueden resolver localhost o alcanzar un gateway local.
Eso no es conectividad hacia tu dependencia. Es una verdad parcial.
Aquí está la comprobación que parece verde pero no lo es:
cr0x@server:~$ getent hosts postgres.internal
10.20.0.15 postgres.internal
Significado: la resolución DNS (o /etc/hosts) funciona. Eso no dice nada sobre routing, firewall o enlace.
Decisión: Siempre combina comprobaciones de resolución de nombres con comprobaciones de alcanzabilidad: ip route get, ping, nc, ss.
cr0x@server:~$ ip route get 10.20.0.15
RTNETLINK answers: Network is unreachable
Significado: el kernel no tiene ruta. Eso es de nivel inferior que la app y mayor confianza que cualquier “debería funcionar”.
Decisión: Arregla netplan/rutas. No toques TimeoutStartSec hasta que el host pueda enrutar a sus dependencias.
Paso 5: La reparación real (y la no-reparación)
La no-reparación: aumentar TimeoutStartSec a cinco minutos. Eso solo le da más tiempo a tu outage para ser misterioso.
La reparación es restaurar la red, y luego ajustar el comportamiento del servicio:
si Postgres está caído, ¿debería el host arrancar? Usualmente sí. ¿El servicio debe seguir intentando? Usualmente sí.
¿Debería bloquearse 90 segundos y luego morir? Depende de si quieres la unidad “activa” sin BD.
En la práctica, eliges una de dos:
- Hacerla resistente: permitir que arranque y sirva funcionalidad parcial, o seguir intentando sin fallar readiness.
- Hacerlo explícito: fallar rápido, pero añadir logs claros y hacer que la orquestación lo conozca.
Si el demonio no soporta sd_notify, cambiar a Type=simple frecuentemente evita timeouts falsos:
cr0x@server:~$ sudo systemctl edit acme-api.service
# (editor opens)
cr0x@server:~$ cat /etc/systemd/system/acme-api.service.d/override.conf
[Service]
Type=simple
TimeoutStartSec=30
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart acme-api.service
Significado: Estás alineando las expectativas de readiness de systemd con la realidad. Además, acortaste la ventana de “colgado misterioso”.
Decisión: Haz esto solo si el servicio realmente no notifica. Si sí notifica, mantén Type=notify y repara la ruta de readiness.
Broma #2: Aumentar TimeoutStartSec es como mover el detector de humo más lejos. La cocina sigue en llamas; solo que ahora no lo oyes.
Tres micro-historias corporativas (y la lección que realmente necesitas)
Micro-historia #1: El outage causado por una suposición incorrecta
Una empresa SaaS mediana tenía un servicio worker en background que procesaba eventos de facturación. Era “simple”:
leer de una cola, escribir en Postgres, emitir métricas. Funcionó meses sin drama.
Durante una sprint de hardening de seguridad, alguien actualizó el archivo de unidad:
After=network.target y Wants=network.target, porque parecía “correcto” y ya estaba en otras unidades.
La suposición fue que network.target significaba “la red está arriba”.
Una semana después, la empresa migró una VLAN e introdujo una espera DHCP más larga en un subconjunto de nodos.
Esos nodos arrancaron, iniciaron el worker inmediatamente y el worker intentó conectar antes de que existiera la ruta.
El worker tenía un timeout de arranque de 60 segundos y salió con código no cero. systemd lo reinició agresivamente. StartLimitHit se disparó.
Lo feo: los dashboards mostraban el worker “en ejecución” en la mayoría de nodos. En los nodos afectados estaba “fallido”, pero nadie tenía alertas sobre el estado de la unidad.
La facturación se retrasó. Finanzas lo notó antes que ingeniería, lo cual es un tipo especial de vergüenza.
Lo arreglaron haciendo explícita la conectividad: o bien esperar una dependencia específica (un endpoint BD alcanzable mediante un script),
o hacer que el worker arranque y siga reintentando sin fallar la readiness de systemd. También dejaron de usar network.target como manta de consuelo.
Lección: No codifiques folklore en los archivos de unidad. Si dependes de conectividad, define qué significa “listo” y mídelo.
Micro-historia #2: La optimización que salió mal
Un equipo de TI empresarial quería arranques más rápidos en una flota de servidores Ubuntu. Alguien notó unos segundos gastados en “wait online”
y decidió que era innecesario. Deshabilitaron systemd-networkd-wait-online.service para ganar tiempo de arranque.
Funcionó—el arranque fue más rápido. Luego un servicio dependiente de almacenamiento empezó a fallar después de reinicios. No siempre. Lo suficiente para ser caro.
El servicio montaba un LUN iSCSI y luego arrancaba una base de datos encima. Con wait-online deshabilitado, el login iSCSI competía con la configuración de la red.
A veces ganaba. A veces se estrellaba.
La primera respuesta del equipo fue aumentar timeouts de inicio de la BD. Eso hizo que la BD “arrancara” más a menudo,
pero creó un modo de fallo peor: la base de datos arrancaba en un directorio vacío si el montaje no estaba presente, inicializaba un nuevo cluster
y servía datos sin sentido hasta que alguien lo notaba.
La solución final no fue “volver a activar wait-online globalmente.” Hicieron ordering y requirements precisos:
la unidad de BD requería la unidad de montaje; la unidad de montaje requería una ruta de red; y usaron comprobaciones de dependencia por unidad.
El tiempo de arranque siguió siendo rápido en máquinas que no necesitaban la red. Las que sí, fueron correctas.
Lección: Las optimizaciones globales de arranque deben tratarse como cambiar una biblioteca compartida. Si no puedes describir el grafo de dependencias, no “lo aceleres”.
Micro-historia #3: La práctica aburrida pero correcta que salvó el día
Un equipo de plataforma ejecutaba un conjunto de servicios internos con archivos de unidad proporcionados por el proveedor. Nunca editaron archivos en /usr/lib/systemd/system.
Nunca. En su lugar, cada cambio iba en overrides drop-in en /etc/systemd/system/*.d/.
Esto parecía burocrático para algunos ingenieros—hasta que una actualización urgente de paquete llegó un viernes por la noche.
La actualización reemplazó la unidad del proveedor y cambió una bandera de ExecStart. En hosts donde la gente había editado unidades del proveedor directamente,
sus cambios fueron sobrescritos silenciosamente y los servicios fallaron.
En los hosts gestionados de la forma aburrida, los drop-ins siguieron aplicándose limpias. Tras la actualización, los servicios se reiniciaron con los nuevos defaults del proveedor
más los overrides del equipo. El único trabajo requerido fue validar comportamiento—no recuperar de drift de configuración.
La revisión post-incidente fue tranquila. El equipo no ganó puntos por heroísmo. Evitaron heroísmos. Ese es el trabajo.
Lección: Las prácticas aburridas (drop-ins, verificar, recargar, logs consistentes) son las que sobreviven a las actualizaciones.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: “Failed with result ‘timeout’” durante el arranque
Causa raíz: desacuerdo de readiness (Type=notify sin sd_notify), o el servicio bloquea en una dependencia (BD, DNS, montaje), o StartTimeout demasiado corto para trabajo real.
Solución: Determina si el proceso está haciendo trabajo útil. Si no hay soporte sd_notify, cambia a Type=simple.
Si está esperando una dependencia, arregla la dependencia o cambia el comportamiento del servicio para reintentar después de arrancar en vez de bloquear la readiness.
2) Síntoma: “failed at step EXEC”
Causa raíz: ruta de ExecStart incorrecta, binario faltante, permiso de ejecución, intérprete faltante (p. ej. script con shebang malo), o sistema de archivos no montado.
Solución: Revisa systemctl status y el archivo de unidad; valida que el archivo exista y sea ejecutable; verifica unidades de montaje; inspecciona denegaciones de AppArmor.
3) Síntoma: “start request repeated too quickly” / start-limit-hit
Causa raíz: crash loop o fallo rápido, exacerbado por Restart=always/on-failure. El limitador de systemd se dispara.
Solución: Lee los logs del primer fallo, no del último. Arregla la config/puerto/secretos subyacentes. Luego systemctl reset-failed.
Opcionalmente ajusta StartLimit, pero no los uses para ocultar un bucle.
4) Síntoma: servicio funciona cuando lo ejecuto manualmente, falla bajo systemd
Causa raíz: entorno distinto (PATH, directorio de trabajo, ulimits), usuario distinto, opciones de sandboxing, permisos faltantes.
Solución: Usa systemd-run con User/Group para reproducir; revisa WorkingDirectory, EnvironmentFile y flags de hardening de la unidad.
5) Síntoma: servicio falla solo después de reinicio, no después de reinicio manual
Causa raíz: problema de orden en boot: montaje no listo, red no configurada, secretos no disponibles, hora no sincronizada, servicio dependencia no iniciado aún.
Solución: Usa systemd-analyze critical-chain e inspecciona dependencias. Sustituye After=network.target por orden y requerimientos correctos (unidad de montaje, unidad de dependencia específica o una pre-comprobación ligera).
6) Síntoma: servicio “arranca” pero sale inmediatamente y systemd reporta éxito
Causa raíz: Type equivocado (p. ej. forking vs simple), o ExecStart lanza un wrapper que sale mientras el demonio continúa (o nunca continúa).
Solución: Verifica el modelo del demonio. Usa Type=forking con PIDFile solo cuando el demonio realmente hace fork. Prefiere Type=simple o notify para demonios modernos.
7) Síntoma: todo “parece bien”, pero el servicio no puede acceder a un archivo o socket
Causa raíz: denegación de AppArmor, sandboxing de systemd (ProtectSystem, ReadOnlyPaths, PrivateTmp), o desajuste de propiedad de archivos tras despliegue.
Solución: Busca denegaciones en el journal del kernel; ajusta la política o sandboxing; asegura propiedad y permisos correctos.
Listas de verificación / plan paso a paso
Checklist A: El triage de 90 segundos (servicio único que falla)
- Ejecuta
systemctl status UNIT --no-pager. Captura Result (timeout/exit-code/start-limit-hit) y cualquier “step” (EXEC). - Ejecuta
journalctl -u UNIT -b -n 200 --no-pager. Busca la última línea significativa de la app. - Ejecuta
systemctl cat UNIT. AnotaType=,TimeoutStartSec=,User=y la ruta de ExecStart. - Colócalo en un cubo: problema EXEC, exit-code, timeout/espera, dependencia, política o limitación por tasa.
- Elige la siguiente herramienta según el cubo (no improvises).
Checklist B: Workflow “el arranque está atascado”
systemctl --failed --no-pagerpara listar fallos obvios tempranos (montajes, red, resolución de nombres).systemd-analyze critical-chainpara identificar qué retrasó alcanzar el target por defecto.- Arregla la dependencia rota más temprana primero (montaje/red) antes de reiniciar servicios aguas abajo.
- Si necesitas el host arriba inmediatamente: enmascara temporalmente la unidad no crítica que falla, arranca a multi-user y revierte la máscara después del arreglo.
Checklist C: Disciplina de reparación segura para reinicios (evita heridas autoinfligidas)
- Nunca edites archivos de proveedor en
/usr/lib/systemd/systemo/lib/systemd/system. Usa drop-ins consystemctl edit. - Después de cambios en unidades:
systemctl daemon-reload, luego reinicia la unidad. - Usa
systemd-analyze verifyen unidades que tocaste. - Registra qué cambiaste y por qué. El tú del futuro también es SRE y está cansado.
Checklist D: Arranques de servicios conscientes del almacenamiento (porque fallos de almacenamiento se disfrazan de fallos de servicio)
- Si un servicio usa una ruta como
/mnt/data, asegura que exista una unidad de montaje y que el servicio la requiera. - Revisa montajes fallidos temprano con
systemctl --failed. - Nunca dejes que una base de datos se inicialice en un directorio sin montar. Añade comprobaciones explícitas o requirements.
- Si el montaje es remoto (NFS/iSCSI), trátalo como dependencia de red y diseña para fallos parciales.
Preguntas frecuentes
1) ¿Por qué systemd dice “Failed to start” cuando el binario claramente se ejecutó?
Porque “start” incluye readiness. Si se usa Type=notify, systemd espera una señal de readiness.
Si la app bloquea en una dependencia, systemd puede hacer timeout y matarla aunque estuviera “haciendo algo”.
2) ¿Debo siempre añadir After=network-online.target?
No. Muchos servicios no necesitan conectividad total al iniciar, y esperar “online” puede ralentizar el arranque o introducir nuevos modos de fallo.
Agrégalo solo cuando el servicio realmente requiera conectividad de red para ser funcional, y prefiere dependencias más específicas cuando sea posible.
3) ¿Cuál es la diferencia entre After= y Requires=?
After= es solo orden: “arráncame después”. Requires= es un requisito: “si eso falla, yo fallo también”.
Confundirlas crea deadlocks en el arranque (demasiados requisitos) o condiciones de carrera (orden sin asegurar existencia de la dependencia).
4) ¿Qué significa habitualmente “failed at step EXEC”?
systemd no pudo ejecutar el comando configurado. Causas comunes: ruta incorrecta, binario faltante, sin bit de ejecución, permisos de usuario,
intérprete faltante para scripts o una ruta requerida no montada.
5) ¿Cómo sé si AppArmor está bloqueando mi servicio?
Busca denegaciones en el journal del kernel: journalctl -k -b | grep -i apparmor.
Si ves entradas DENIED que coinciden con el binario de tu servicio, encontraste un problema de política, no de aplicación.
6) ¿Cuándo debería aumentar TimeoutStartSec?
Solo cuando el servicio legítimamente necesita más tiempo para volverse listo y entiendes por qué.
Si está esperando una dependencia inestable, un timeout mayor puede hacer la recuperación más lenta y los fallos más difíciles de detectar.
7) ¿Por qué un bucle de reinicio de servicio se convierte en start-limit-hit?
systemd limita los reinicios para evitar thrash. Si una unidad falla repetidamente en un intervalo corto, deja de intentar.
Arregla el problema subyacente y luego limpia el contador con systemctl reset-failed.
8) El servicio funciona cuando lo ejecuto manualmente. ¿Por qué no bajo systemd?
systemd lo ejecuta como un usuario configurado con un entorno específico, directorio de trabajo, ulimits, restricciones de cgroup y posiblemente sandboxing.
Reproduce usando systemd-run con el mismo User/Group para acotar la diferencia.
9) ¿Cómo sé si el problema es upstream (dependencia) en vez del servicio?
Si los logs de la unidad muestran “waiting for …” (BD, DNS, montaje), o ves unidades de montaje/red fallidas en systemctl --failed,
trátalo como upstream hasta que se demuestre lo contrario. Arregla la primera falla en la cadena.
Pasos siguientes que puedes hacer hoy
Si quieres que los incidentes “Failed to start” dejen de comerse tus tardes, haz esto en orden:
- Estandariza los comandos de triage entre tu equipo:
systemctl status,journalctl -u,systemctl cat,systemd-analyze critical-chain,systemctl --failed. - Haz honesta la readiness de las unidades: no uses
Type=notifya menos que el demonio realmente notifique; no escondas esperas de dependencia detrás de timeouts largos. - Conecta dependencias explícitamente: si necesitas un montaje, requiérelo. Si necesitas una ruta, pruébala (o diseña para reintentos sin bloquear el arranque).
- Usa drop-in overrides para cada cambio operativo. Mantén los unit files del proveedor intactos para que las actualizaciones no te sorprendan.
- Añade alertas sobre fallos de unidad para los servicios que importan. El journal es genial, pero no te despierta por la noche.
El flujo de triage más rápido no es una bolsa de comandos. Es un hábito: identifica la clase de fallo, lee la unidad, lee los logs,
sigue la cadena de dependencias y arregla lo primero que esté roto. Todo lo demás es teatro.