Escribir un unit file .service para tus propios servicios

Un unit file de tipo .service es el fichero de texto que le dice a systemd cómo arrancar, supervisar y parar un proceso. Cuando ejecutas systemctl start algo, systemd lee ese fichero, interpreta sus directivas y actúa en consecuencia. No hay magia: es configuración declarativa que reemplaza los viejos scripts de init, con la ventaja de que el kernel y systemd se reparten la responsabilidad de supervisión, logging y dependencias de forma uniforme para todos los servicios del sistema.

El diseño en tres secciones —[Unit], [Service], [Install]— no es arbitrario. [Unit] describe metadatos y relaciones con otras units. [Service] define el comportamiento de ejecución. [Install] indica qué targets deben “jalar” este servicio cuando se habilita con enable. Esta separación existe porque systemd distingue entre lo que un servicio es, lo que hace y cómo se integra en el grafo de arranque. Si confundes esas capas —por ejemplo, poniendo lógica de arranque en [Unit]— el resultado es un servicio que falla silenciosamente o que se ignora en el arranque automático.

Usar esto bien importa en cuanto tienes cualquier proceso que necesite vivir más allá de una sesión de terminal: una API propia, un worker de colas, un script de sincronización periódica, un proxy inverso compilado a mano. Si lo lanzas desde un screen o un nohup, pierdes reinicio automático, integración con el journal, y control de recursos. Si metes la configuración en el unit file del paquete del sistema, la próxima actualización te la pisa.

Lo que se rompe cuando esto falla: si ExecStart= no usa path absoluto, el servicio no arranca. Si omites daemon-reload tras escribir o modificar el fichero, systemd sigue leyendo la versión anterior en memoria y tus cambios no tienen efecto. Si corres el proceso como root pudiendo evitarlo, una vulnerabilidad en la aplicación tiene acceso completo al sistema. Si pones WantedBy= pero nunca ejecutas systemctl enable, el servicio no sobrevive al reinicio.

# /etc/systemd/system/sincronizador.service
#
# Servicio de ejemplo: un script Python que sincroniza datos
# con una API remota y corre indefinidamente en foreground.

[Unit]
Description=Sincronizador de datos con API remota
# After= no crea dependencia fuerte; solo garantiza orden de arranque.
# Si network.target no levanta, este servicio se salta para esa sesión
# pero systemd no lo considera fallido por eso.
After=network.target

[Service]
Type=simple
# Path absoluto obligatorio. Sin él, systemd no sabe dónde buscar.
ExecStart=/opt/sincronizador/venv/bin/python /opt/sincronizador/main.py

# Correr como usuario sin privilegios dedicado a esta aplicación.
User=sincronizador
Group=sincronizador

# El directorio de trabajo desde el que el proceso ve rutas relativas.
WorkingDirectory=/opt/sincronizador

# Variables de entorno inline para valores no sensibles.
Environment="LOG_LEVEL=info"
Environment="API_ENDPOINT=https://api.ejemplo.com/v2"

# Para secretos o configuración que cambia por entorno,
# un fichero externo evita que los valores queden en el unit file.
# systemd lo lee antes de lanzar el proceso.
EnvironmentFile=/etc/sincronizador/env

# Reiniciar solo si el proceso termina con código de error o señal inesperada.
# "always" reintenta incluso en salida limpia, útil para daemons eternos.
Restart=on-failure

# Esperar 5 segundos antes de reintentar, para no aplastar una API caída.
RestartSec=5

# Limitar el número de reinicios en una ventana de tiempo.
# 5 intentos en 60 segundos: si se supera, systemd marca el servicio
# como "failed" y deja de reintentar hasta que intervienes manualmente.
StartLimitIntervalSec=60
StartLimitBurst=5

[Install]
# multi-user.target es el equivalente al runlevel 3: sistema sin GUI.
# Esta línea crea el symlink en /etc/systemd/system/multi-user.target.wants/
# cuando ejecutas `systemctl enable`. Sin ella, enable no hace nada útil.
WantedBy=multi-user.target

Después de escribir ese fichero:

# Crear el usuario sin shell de login y sin directorio home en /home
sudo useradd --system --no-create-home --shell /usr/sbin/nologin sincronizador

# Dar al usuario acceso a los ficheros de la aplicación
sudo chown -R sincronizador:sincronizador /opt/sincronizador

# Crear el fichero de variables de entorno con permisos restrictivos
sudo mkdir -p /etc/sincronizador
sudo install -o sincronizador -g sincronizador -m 640 /dev/null /etc/sincronizador/env
echo 'API_TOKEN=s3cr3t0' | sudo tee -a /etc/sincronizador/env

# Obligatorio tras cualquier creación o modificación de un unit file.
# Sin esto, systemd opera con la versión cacheada en memoria.
sudo systemctl daemon-reload

# Habilitar (crear symlink para el arranque) y arrancar en un solo paso.
sudo systemctl enable --now sincronizador.service

# Verificar estado, PID, uptime y las últimas líneas del journal.
systemctl status sincronizador.service

# Seguir el log en tiempo real mientras pruebas.
journalctl -u sincronizador.service -f

Type=simple le dice a systemd que el proceso que arranca ExecStart= es el proceso principal. systemd lo considera listo en el momento en que el binario se ejecuta, sin esperar ninguna señal. Es correcto para cualquier proceso que corra en foreground y no se bifurque. Si tu aplicación hace un fork para demonizarse (como algunos servidores escritos al estilo UNIX clásico), necesitas Type=forking y posiblemente PIDFile=. Si es un script que hace trabajo y termina, Type=oneshot con RemainAfterExit=yes evita que systemd lo marque como caído cuando termina limpiamente.

EnvironmentFile=/etc/sincronizador/env con permisos 640 y propietario sincronizador significa que el token API no aparece ni en el unit file ni en la salida de systemctl show, que es legible por cualquier usuario del sistema. El unit file puede estar en un repositorio de configuración; el fichero de entorno no debería estarlo.

Restart=on-failure combinado con StartLimitBurst=5 y StartLimitIntervalSec=60 crea un comportamiento importante: systemd reintenta automáticamente si el proceso cae, pero si cae cinco veces en un minuto —señal de que algo está fundamentalmente roto— para de reintentar y marca el servicio como failed. Esto evita que un proceso mal configurado consuma CPU en un loop infinito de reinicios. Para resetear ese contador y volver a intentarlo tras investigar: systemctl reset-failed sincronizador.service.

La línea WantedBy=multi-user.target en [Install] es la que convierte systemctl enable en algo concreto: crea un symlink en /etc/systemd/system/multi-user.target.wants/sincronizador.service. Puedes verificarlo con ls -la /etc/systemd/system/multi-user.target.wants/. Si ese symlink no existe, el servicio no arranca en el próximo boot, independientemente de lo que diga el unit file. systemctl disable simplemente borra ese symlink.

67

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio