Guía completa de servicios systemd: del script al daemon robusto
Cómo convertir un script en un servicio systemd production-ready con reinicio automático, límites de recursos, sandboxing y logs en journald.
Por qué systemd (aunque le tengas manía)
systemd es el init system dominante en Linux desde 2015: Ubuntu, Debian, Fedora, RHEL, Arch, openSUSE. Puedes tenerle manía por el ámbito que abarca, pero comparado con SysV init es un salto brutal: arranque paralelo, dependencias declarativas, socket activation, cgroups nativos, journald como logging estructurado, y capacidades de sandboxing que sólo hace 10 años requerían SELinux o AppArmor.
Un unit file mínimo funcional
Guardar como /etc/systemd/system/mi-app.service:
[Unit]
Description=Mi aplicación Node.js
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/mi-app/server.js
User=mi-app
Group=mi-app
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Activa el servicio:
sudo systemctl daemon-reload
sudo systemctl enable --now mi-app
sudo systemctl status mi-app
Las tres secciones que debes dominar
[Unit] — metadatos y dependencias:
Description=— línea corta descriptivaAfter=— orden de arranque (no dependencia dura)Requires=— dependencia dura: si falla, este servicio no arrancaWants=— dependencia blanda: intenta arrancar el otro, pero continúa aunque falle
[Service] — cómo se ejecuta:
Type=simple(default),forking(procesos que hacen daemon),oneshot(script que termina),notify(usa sd_notify)ExecStart=— comando principal (ruta absoluta obligatoria)ExecReload=— comando parasystemctl reloadRestart=—no,on-failure,always,on-abnormalRestartSec=— espera antes de reintentarEnvironment=/EnvironmentFile=— variables
[Install] — cuándo se activa con enable:
WantedBy=multi-user.target— arranque normalWantedBy=graphical.target— solo con GUI
Hardening: sandboxing que sí uses
systemd ofrece decenas de directivas de aislamiento equivalentes a un mini contenedor:
[Service]
# Filesystem
ProtectSystem=strict # /usr, /boot, /etc read-only
ProtectHome=yes # /home, /root, /run/user invisibles
PrivateTmp=yes # /tmp aislado
ReadWritePaths=/var/lib/mi-app /var/log/mi-app
# Procesos y capabilities
NoNewPrivileges=yes # bloquea setuid escalation
CapabilityBoundingSet= # sin capabilities
AmbientCapabilities=
# Kernel / dispositivos
PrivateDevices=yes # solo /dev/null, zero, random...
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
# Red
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
IPAddressDeny=any
IPAddressAllow=localhost 10.0.0.0/8
# Recursos
MemoryMax=512M
CPUQuota=50%
TasksMax=100
Analiza el nivel de exposición de tu unit con:
systemd-analyze security mi-app
Muestra un score de 0 (paranoid) a 10 (unsafe) y sugiere qué añadir. Objetivo razonable: <4.
Type=notify y readiness real
Si tu app tarda unos segundos en estar lista (conectar a DB, calentar cache), Type=simple marcará como activo el servicio en cuanto el proceso arranque, aunque no acepte peticiones aún. Para readiness real, usa Type=notify y llama a sd_notify(READY=1) desde tu código cuando estés listo:
// Node.js con systemd-notify
const notify = require('systemd-notify');
server.listen(3000, () => notify.ready());
Ahora systemctl start bloquea hasta que la app está lista.
Timers systemd: la alternativa moderna a cron
Un timer es un unit que dispara otro unit en un horario. Ejemplo diario a las 3 am:
# /etc/systemd/system/backup.timer
[Unit]
Description=Backup diario
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true # ejecuta si perdió el disparo por apagado
RandomizedDelaySec=300 # jitter para no colisionar
[Install]
WantedBy=timers.target
Con backup.service a su lado que hace el trabajo. Ventajas sobre cron: logs en journald, políticas de reinicio, dependencias, y OnCalendar mucho más legible que la sintaxis 0 3 *.
journald: logs bien hechos
Todo lo que tu app escriba a stdout/stderr aparece automáticamente en journald:
journalctl -u mi-app -f # follow en tiempo real
journalctl -u mi-app --since '1 hour ago'
journalctl -u mi-app -o json | jq # JSON para procesar
journalctl -u mi-app --priority=err
Configura tamaño máximo en /etc/systemd/journald.conf:
SystemMaxUse=2G
MaxRetentionSec=1month
Para ingesta central, envía a Loki con promtail o a Elasticsearch con filebeat --input.type=journald.
Recarga sin downtime: reload vs restart
Muchas apps aceptan una señal para recargar configuración sin cortar peticiones (típicamente SIGHUP para Nginx o SIGUSR2 para reload gracioso). Configúralo:
[Service]
ExecReload=/bin/kill -HUP $MAINPID
Ahora systemctl reload mi-app recarga sin matar el proceso.
Errores comunes
- Rutas relativas en ExecStart — usa siempre absolutas. systemd no expande
$PATHde tu shell. - Olvidar
daemon-reloadtras editar el unit. systemd no releerá el archivo automáticamente. - Loops de reinicio — si el servicio falla al arrancar y
Restart=always, se reinicia cada X segundos indefinidamente. AñadeStartLimitBurst=5yStartLimitIntervalSec=60. - Depender de
network.target— no significa "red conectada" sino "red configurada". Usanetwork-online.targetsi necesitas conectividad real.
Generar unit files rápido
Nuestro generador de systemd unit produce el unit completo con hardening por defecto: solo indicas comando, usuario y política de reinicio. Un buen punto de partida.
Bien configurado, un servicio systemd sobrevive reinicios, contiene daños en caso de compromiso, expone métricas y logs bien y no requiere mantenimiento diario. Vale la tarde que cuesta aprenderlo.
Herramientas relacionadas
Genera unidades .service listas para instalar en /etc/systemd/system.
Construye y explica expresiones cron interactivamente con presets habituales.
Convierte entre notación simbólica (rwxr-xr-x) y octal (755) con explicación.