Linux··13 min de lectura

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 descriptiva
  • After= — orden de arranque (no dependencia dura)
  • Requires= — dependencia dura: si falla, este servicio no arranca
  • Wants= — 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 para systemctl reload
  • Restart=no, on-failure, always, on-abnormal
  • RestartSec= — espera antes de reintentar
  • Environment= / EnvironmentFile= — variables

[Install] — cuándo se activa con enable:

  • WantedBy=multi-user.target — arranque normal
  • WantedBy=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 $PATH de tu shell.
  • Olvidar daemon-reload tras 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ñade StartLimitBurst=5 y StartLimitIntervalSec=60.
  • Depender de network.target — no significa "red conectada" sino "red configurada". Usa network-online.target si 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

Sigue leyendo