systemd gestiona servicios en la práctica totalidad de distribuciones Linux modernas mediante unidades declarativas: archivos `.service`, `.timer`, `.socket` o `.mount` que describen qué ejecutar, cuándo y con qué restricciones de reinicio y aislamiento. Frente a un script de init tradicional, una unidad bien escrita gestiona reinicios automáticos, dependencias de arranque y límites de recursos con unas pocas líneas declarativas, sin lógica de shell propensa a fallos silenciosos. Este generador de unidades systemd construye el archivo `.service` o `.timer` completo —secciones `[Unit]`, `[Service]` y `[Install]`— a partir de un formulario, evitando los errores de sintaxis que provocan que `systemctl daemon-reload` acepte una unidad rota sin avisar hasta que falla al arrancar.
Anatomía de una unidad .service: Unit, Service e Install
La sección [Unit] describe metadatos y dependencias: Description, After=network-online.target para esperar a que la red esté lista, y Requires= si el servicio no tiene sentido sin otra unidad activa. [Service] define cómo ejecutar el proceso: ExecStart con la ruta absoluta del binario (systemd no busca en el PATH del usuario), User/Group para no correr como root innecesariamente, y Restart=on-failure para reinicios automáticos ante caídas. [Install] sólo importa cuando habilitas la unidad con systemctl enable: WantedBy=multi-user.target la engancha al arranque normal del sistema en modo multiusuario sin entorno gráfico.
Tipos de servicio: simple, forking, notify y oneshot
Type=simple (el valor por defecto) asume que el proceso lanzado por ExecStart es el proceso principal y permanece en primer plano; es el más habitual para binarios modernos que no hacen fork. Type=forking es para daemons clásicos que arrancan, hacen fork a segundo plano y terminan el proceso padre; systemd necesita entonces PIDFile= para saber qué proceso vigilar. Type=notify exige que el propio programa envíe una notificación vía sd_notify() cuando ya está listo para recibir tráfico, lo que permite a systemd secuenciar correctamente servicios dependientes que de otro modo arrancarían antes de tiempo. Type=oneshot es para tareas que terminan y no dejan proceso residente, como un script de backup, y suele combinarse con RemainAfterExit=yes si otras unidades dependen de que ya se haya ejecutado.
Restart y límites: evitar el bucle de reinicio infinito
Restart=on-failure reinicia el servicio cuando termina con código de salida distinto de cero o por una señal, pero sin RestartSec y StartLimitBurst un binario que falla al arrancar puede entrar en un bucle de reinicios cada pocos milisegundos, saturando CPU y logs. RestartSec=5 introduce una espera entre intentos, y StartLimitIntervalSec=60 junto con StartLimitBurst=5 en [Unit] detiene los reintentos si el servicio falla más de cinco veces en sesenta segundos, marcando la unidad como failed en lugar de seguir reintentando indefinidamente. Este límite es el que evita que un despliegue roto convierta al servidor en un generador infinito de entradas de journal.
Aislamiento y hardening: ProtectSystem, NoNewPrivileges, PrivateTmp
systemd permite endurecer un servicio sin contenedores: ProtectSystem=strict monta el sistema de archivos raíz como sólo lectura salvo las rutas listadas en ReadWritePaths=, PrivateTmp=yes da al servicio su propio /tmp aislado del resto del sistema, y NoNewPrivileges=yes impide que el proceso escale privilegios vía setuid aunque el binario los tenga. ProtectHome=yes oculta los directorios de usuarios, relevante para servicios que no necesitan acceder a /home. Estas directivas cuestan una línea cada una y reducen de forma sustancial el radio de impacto si el proceso es comprometido, sin necesidad de levantar un contenedor completo sólo para aislar un servicio.
Timers de systemd frente a cron
Un .timer empareja con una unidad .service del mismo nombre base y la dispara según OnCalendar= (sintaxis similar a cron pero más legible, por ejemplo OnCalendar=*-*-* 03:00:00 para las 3 de la madrugada todos los días) o OnBootSec=/OnUnitActiveSec= para intervalos relativos. La ventaja sobre cron es que el timer se integra con el resto de systemd: Persistent=true ejecuta el job pendiente si la máquina estuvo apagada en el momento programado, cada ejecución queda registrada en el journal con su propio estado de éxito o fallo, y systemctl list-timers muestra de un vistazo la próxima ejecución de todos los timers activos, algo que cron no ofrece sin herramientas adicionales.
Diagnosticar un servicio que no arranca
El primer comando ante un fallo es systemctl status nombre.service, que muestra el estado (active, failed, activating), el PID principal si lo hay, y las últimas líneas de log incrustadas. Para el histórico completo, journalctl -u nombre.service filtra únicamente los mensajes de esa unidad, y añadiendo -f se sigue en tiempo real como con tail -f. journalctl -u nombre.service --since "10 min ago" acota la ventana temporal cuando el log es extenso. Si el fallo es de sintaxis en el propio archivo de unidad y no del programa, systemd-analyze verify nombre.service detecta directivas mal escritas o secciones inexistentes antes incluso de intentar arrancarlo.
Recargar la configuración sin reiniciar el sistema
Después de crear o modificar un archivo en /etc/systemd/system/, systemctl daemon-reload es obligatorio para que systemd relea las unidades desde disco; sin este paso, systemctl restart seguirá usando la definición cacheada en memoria y los cambios no se aplicarán aunque el archivo ya esté actualizado. systemctl enable nombre.service crea el symlink en el target correspondiente para que arranque en el próximo boot, pero no lo arranca ahora; systemctl enable --now nombre.service hace ambas cosas en un solo comando, que es el patrón más habitual al desplegar un servicio nuevo.
Salidas reales de ejemplo
● miapp.service - Servicio API de mi aplicación
Loaded: loaded (/etc/systemd/system/miapp.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2024-05-31 09:12:04 UTC; 2h 14min ago
Main PID: 48213 (node)
Tasks: 11 (limit: 4678)
Memory: 82.4M
CPU: 3.912s
CGroup: /system.slice/miapp.service
└─48213 /usr/bin/node /opt/miapp/dist/server.js
may 31 09:12:04 srv1 systemd[1]: Started Servicio API de mi aplicación.
may 31 09:12:05 srv1 node[48213]: Server listening on port 3000Fíjate en el campo Active (running) con el tiempo desde el arranque y en Main PID, que identifica el proceso vigilado por systemd.
may 31 10:02:11 srv1 systemd[1]: miapp.service: Main process exited, code=exited, status=1/FAILURE
may 31 10:02:11 srv1 systemd[1]: miapp.service: Failed with result 'exit-code'.
may 31 10:02:16 srv1 systemd[1]: miapp.service: Scheduled restart job, restart counter is at 5.
may 31 10:02:16 srv1 systemd[1]: miapp.service: Start request repeated too quickly.
may 31 10:02:16 srv1 systemd[1]: miapp.service: Failed with result 'start-limit-hit'.
may 31 10:02:16 srv1 systemd[1]: Failed to start Servicio API de mi aplicación.El mensaje 'start-limit-hit' confirma que StartLimitBurst y StartLimitIntervalSec han evitado un bucle de reinicios infinito.
$ systemd-analyze verify /etc/systemd/system/miapp.service
miapp.service:12: Unknown key name 'ExecStar' in section 'Service', ignoring.
miapp.service: Service has no ExecStart= setting, which is only allowed for Type=oneshot services.El verificador señala la línea concreta con la directiva mal escrita, sin necesidad de intentar arrancar el servicio.
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sat 2024-06-01 03:00:00 UTC 16h left Fri 2024-05-31 03:00:00 UTC 7h ago backup.timer backup.service
1 timers listed.Muestra la próxima ejecución programada, la última completada y si el timer está activo (LEFT/PASSED).
Tipos de unidad Type= en la sección [Service]
| Type | Comportamiento esperado del proceso | Ejemplo típico |
|---|---|---|
| simple | Permanece en primer plano tras ExecStart | APIs Node.js, binarios Go compilados |
| forking | Hace fork y el padre termina; requiere PIDFile | Daemons clásicos tipo nginx en modo daemon |
| notify | Avisa a systemd vía sd_notify() cuando está listo | Servicios que deben secuenciarse con precisión |
| oneshot | Ejecuta y termina, sin proceso residente | Scripts de backup o migraciones puntuales |
El tipo correcto depende de cómo se comporta el binario al arrancar.
cron frente a systemd timer
| Característica | cron | systemd timer |
|---|---|---|
| Registro de ejecución | Sólo si el propio script escribe log | Integrado en journalctl automáticamente |
| Recuperar ejecuciones perdidas | No, sin herramientas externas | Sí, con Persistent=true |
| Dependencias con otros servicios | No soportado | Sí, vía After=/Requires= |
| Ver próxima ejecución | No sin parsear el crontab manualmente | systemctl list-timers |
Ambos programan tareas periódicas, pero el timer se integra con el resto del ecosistema systemd.
Casos de uso comunes
- Desplegar una API Node.js o Python como servicio persistente con reinicio automático ante caídas
- Sustituir una entrada de cron por un timer con registro de ejecuciones en journalctl
- Endurecer un servicio expuesto a internet con ProtectSystem, PrivateTmp y NoNewPrivileges
- Encadenar servicios dependientes (base de datos antes que la API) con After= y Requires=
- Ejecutar un script de backup nocturno con Type=oneshot y Persistent=true por si el servidor está apagado
Buenas prácticas
- Ejecuta siempre `systemctl daemon-reload` tras editar una unidad, antes de reiniciar el servicio
- Define `RestartSec` y `StartLimitBurst` para evitar bucles de reinicio que saturen CPU y journal
- No corras servicios como root si no es imprescindible; usa `User=`/`Group=` dedicados
- Añade `ProtectSystem=strict`, `PrivateTmp=yes` y `NoNewPrivileges=yes` como hardening por defecto
- Usa rutas absolutas en `ExecStart`; systemd no hereda el `PATH` de tu shell interactiva
- Prefiere timers de systemd sobre cron cuando necesites registro de ejecuciones o dependencias con otros servicios