TL;DR — Resumen Rápido
Domina los servicios systemd en Linux: anatomia de unit files, ejemplos con Node.js y Python, politicas de reinicio, limites de recursos y seguridad.
Mantener procesos en ejecucion continua en un servidor Linux solia implicar scripts de SysVinit complicados, configuraciones de supervisor o hacks fragiles en shell. systemd cambio todo eso. Como sistema init predeterminado en todas las distribuciones Linux principales — Ubuntu, Debian, RHEL, Fedora, Arch — systemd ofrece una forma unificada y declarativa de definir exactamente como debe iniciarse, detenerse, reiniciarse, registrar logs e interactuar con el sistema cualquier proceso.
Esta guia cubre todo lo necesario para crear servicios systemd personalizados listos para produccion: la anatomia de un archivo unit, ejemplos concretos para aplicaciones Node.js y Python, tipos de servicio, politicas de reinicio, limites de recursos, directivas de seguridad, activacion por socket, unidades de temporizador como alternativa a cron, y como depurar fallos con journalctl.
Requisitos Previos
Antes de comenzar, asegurate de tener:
- Un sistema Linux con systemd (Ubuntu 16.04+, Debian 8+, CentOS 7+, RHEL 7+, Fedora 15+ o cualquier distribucion moderna)
- Una terminal con acceso
sudoo como root - Familiaridad basica con la linea de comandos y un editor de texto (
nanoovim) - Una aplicacion que quieras ejecutar como servicio (Node.js, Python, binario Go, etc.)
Verifica que systemd este corriendo:
systemctl --version
# Debe mostrar: systemd 249 (o un numero de version similar)
Anatomia del Archivo Unit
Cada servicio que systemd gestiona se describe mediante un archivo unit — un archivo de configuracion en texto plano con formato INI. Los archivos unit de servicio personalizados viven en /etc/systemd/system/. El nombre de archivo termina en .service.
Un archivo unit completo tiene tres secciones:
[Unit]
Description=Mi Servicio de Aplicacion
Documentation=https://ejemplo.com/docs
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=miapp
Group=miapp
WorkingDirectory=/opt/miapp
ExecStart=/usr/bin/node /opt/miapp/server.js
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
La Seccion [Unit]
| Directiva | Proposito |
|---|---|
Description= | Nombre legible que aparece en la salida de systemctl status |
Documentation= | URL o referencia de pagina de manual |
After= | Inicia esta unidad despues de que las unidades listadas esten activas |
Before= | Inicia esta unidad antes que las unidades listadas |
Wants= | Dependencia suave — las unidades listadas se inician pero el fallo es tolerado |
Requires= | Dependencia dura — si la unidad listada falla, esta tambien falla |
La Seccion [Service]
| Directiva | Proposito |
|---|---|
Type= | Modelo de ciclo de vida del proceso (simple, forking, oneshot, notify, idle) |
User= / Group= | Ejecuta el proceso como este usuario/grupo |
WorkingDirectory= | Establece el directorio de trabajo antes de ejecutar |
ExecStart= | El comando a ejecutar (debe ser una ruta absoluta) |
ExecStartPre= | Comandos a ejecutar antes de ExecStart |
ExecStartPost= | Comandos a ejecutar despues de que ExecStart tiene exito |
Restart= | Cuando reiniciar automaticamente |
Environment= | Establece variables de entorno en linea |
EnvironmentFile= | Carga variables de entorno desde un archivo |
StandardOutput= | Donde enviar stdout (journal, file:, append:, null) |
La Seccion [Install]
| Directiva | Proposito |
|---|---|
WantedBy= | Que target habilita este servicio (normalmente multi-user.target) |
Alias= | Nombres adicionales para esta unidad |
Crear un Servicio Node.js
Supongamos que tienes una API Node.js en /opt/miapi/server.js que escucha en el puerto 3000.
Paso 1: Crear un usuario dedicado
sudo useradd --system --no-create-home --shell /usr/sbin/nologin nodeapi
sudo chown -R nodeapi:nodeapi /opt/miapi
Paso 2: Crear el archivo unit
sudo nano /etc/systemd/system/miapi.service
[Unit]
Description=Mi Servicio API Node.js
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=nodeapi
Group=nodeapi
WorkingDirectory=/opt/miapi
EnvironmentFile=/etc/miapi/environment
ExecStart=/usr/bin/node /opt/miapi/server.js
ExecStartPre=/usr/bin/node --check /opt/miapi/server.js
Restart=on-failure
RestartSec=5s
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=miapi
MemoryMax=512M
CPUQuota=50%
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=/opt/miapi/logs /var/lib/miapi
[Install]
WantedBy=multi-user.target
Paso 3: Crear el archivo de entorno
sudo mkdir -p /etc/miapi
sudo nano /etc/miapi/environment
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://usuario:clave@localhost/midb
JWT_SECRET=tu-secreto-aqui
sudo chmod 600 /etc/miapi/environment
sudo chown root:root /etc/miapi/environment
Paso 4: Habilitar e iniciar
sudo systemctl daemon-reload
sudo systemctl enable miapi.service
sudo systemctl start miapi.service
sudo systemctl status miapi.service
Crear un Servicio Python
Para un worker Python en /opt/worker/worker.py:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin pyworker
sudo chown -R pyworker:pyworker /opt/worker
[Unit]
Description=Worker Python en Segundo Plano
After=network.target redis.service
Requires=redis.service
[Service]
Type=simple
User=pyworker
Group=pyworker
WorkingDirectory=/opt/worker
ExecStart=/opt/worker/venv/bin/python -u worker.py
ExecStartPre=/opt/worker/venv/bin/python -c "import redis; redis.Redis().ping()"
EnvironmentFile=/etc/worker/environment
Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=60s
StartLimitBurst=3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=pyworker
MemoryMax=256M
CPUQuota=25%
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=/opt/worker/data
[Install]
WantedBy=multi-user.target
El flag -u en el comando Python deshabilita el buffering de salida para que los logs aparezcan en journald de inmediato.
Tipos de Servicio
| Tipo | Cuando Usar | Como systemd Rastrea la Disponibilidad |
|---|---|---|
simple | La mayoria de apps modernas. ExecStart es el proceso principal. | Considera la unidad iniciada tan pronto como ExecStart hace fork |
exec | Como simple, pero espera a que exec tenga exito | Espera hasta que el binario se ejecuta correctamente |
forking | Daemons tradicionales que hacen fork y el padre termina | Espera a que el proceso padre termine |
oneshot | Scripts que se ejecutan una vez y terminan (tareas de inicializacion) | Espera a que ExecStart termine antes de marcar como activo |
notify | Apps que envian notificacion de disponibilidad via sd_notify() | Espera la notificacion antes de marcar como activo |
Politicas de Reinicio y Limites de Recursos
Politicas de Reinicio
# Reiniciar siempre que el servicio termina (cualquier razon incluida salida limpia)
Restart=always
# Reiniciar solo en fallo (salida distinta de cero, senal, timeout) — NO en salida limpia
Restart=on-failure
# Reiniciar en fallo + timeout watchdog + senales anormales
Restart=on-abnormal
# Nunca reiniciar
Restart=no
Combinado con limitacion de tasa para evitar que un servicio con fallos sature el sistema:
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=5
Esta configuracion permite hasta 5 reinicios en 60 segundos antes de que systemd se rinda y marque el servicio como fallido.
Limites de Recursos
[Service]
MemoryMax=1G # Limite duro — el proceso se termina si se excede
MemoryHigh=800M # Limite suave — el kernel activa la recuperacion de memoria
MemorySwapMax=0 # Deshabilitar swap para este servicio
CPUQuota=200% # Limitar a 2 nucleos CPU completos
LimitNOFILE=65536 # Maximo de descriptores de archivo abiertos
LimitNPROC=512 # Maximo de procesos/hilos
Endurecimiento de Seguridad
[Service]
# Impedir que el proceso obtenga nuevos privilegios via setuid/setgid
NoNewPrivileges=true
# Montar /usr, /boot, /etc como solo lectura para este servicio
ProtectSystem=strict
# Dar al servicio su propio /tmp privado (no compartido con otros procesos)
PrivateTmp=true
# Crear un usuario dinamico sin privilegios (no se necesita useradd manualmente)
DynamicUser=true
# Impedir acceso a directorios /home
ProtectHome=true
# Filtrar las llamadas al sistema que el servicio puede realizar
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# Evitar escritura en variables del kernel
ProtectKernelTunables=true
ProtectKernelModules=true
Analiza la puntuacion de seguridad de cualquier servicio en ejecucion:
systemd-analyze security miapi.service
Unidades de Temporizador: systemd como Alternativa a Cron
Las unidades de temporizador reemplazan los cron jobs con mejor registro de logs, gestion de dependencias y comportamiento de recuperacion.
sudo nano /etc/systemd/system/backup-nocturno.service
[Unit]
Description=Backup Nocturno de Base de Datos
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
StandardError=journal
sudo nano /etc/systemd/system/backup-nocturno.timer
[Unit]
Description=Ejecutar Backup Nocturno a las 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-nocturno.timer
systemctl list-timers --all
systemd vs Otras Herramientas de Gestion de Procesos
| Caracteristica | systemd | SysVinit / init.d | supervisord | PM2 |
|---|---|---|---|---|
| Incluido con el SO | Si (todas las distros principales) | Si (legado) | No (pip install) | No (npm install) |
| Orden de dependencias | Grafo completo | Orden manual | Limitado | No |
| Activacion por socket | Si | No | No | No |
| Limites de recursos cgroup | Si (nativo) | No | No | No |
| Sandboxing a nivel kernel | Si (extenso) | No | No | No |
| Logs centralizados | journald (estructurado) | syslog / archivos | Solo archivos | Archivos PM2 |
| Jobs programados (timers) | Si (unidades timer) | No | No | Si (similar a cron) |
| Integracion con Node.js | Buena | Pobre | Buena | Excelente |
| Curva de aprendizaje | Moderada | Baja | Baja | Muy baja |
Escenario Real
Tienes un servidor de produccion con una aplicacion Python FastAPI que procesa trabajos de una cola Redis y debe reiniciarse automaticamente si falla, sin consumir mas de 512 MB de RAM.
[Unit]
Description=FastAPI Job Worker
After=network.target redis.service
Requires=redis.service
StartLimitIntervalSec=120s
StartLimitBurst=4
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/fastapi-worker
ExecStart=/opt/fastapi-worker/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2
ExecStartPre=/opt/fastapi-worker/venv/bin/python -c "import redis; redis.Redis(host='localhost').ping()"
EnvironmentFile=/etc/fastapi-worker/environment
Restart=on-failure
RestartSec=8s
TimeoutStartSec=30s
TimeoutStopSec=15s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=fastapi-worker
MemoryMax=512M
CPUQuota=100%
LimitNOFILE=32768
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ProtectHome=true
ReadWritePaths=/opt/fastapi-worker/data
[Install]
WantedBy=multi-user.target
Errores Comunes y Casos Limite
ExecStart debe ser una ruta absoluta. No puedes usar built-ins del shell, pipes o redirecciones directamente en ExecStart. Envuelve los comandos complejos en un script.
Expansion de variables de entorno. systemd expande $VAR en ExecStart cuando la variable se establece via Environment= o EnvironmentFile=, pero NO carga archivos de inicializacion del shell. Tu ~/.bashrc nunca se carga.
Signos de porcentaje en archivos unit. En los archivos unit, % es un caracter especificador. Para usar un signo de porcentaje literal, escapalo como %%.
DynamicUser y permisos de archivos. Con DynamicUser=true, el UID asignado cambia entre reinicios. Usa directivas como StateDirectory= o LogsDirectory= para que systemd gestione los directorios persistentes correctamente.
After= vs Requires=. After= solo controla el orden — no crea una dependencia. Requires= crea la dependencia pero no controla el orden. Casi siempre necesitas ambos juntos.
Solucion de Problemas
El servicio no inicia
sudo systemctl status miapi.service
journalctl -u miapi.service -b
journalctl -u miapi.service --since "10 minutes ago"
journalctl -u miapi.service -f
El servicio inicia pero cae de inmediato
journalctl -u miapi.service -n 100 --no-pager
ls -la /usr/bin/node
sudo -u nodeapi /usr/bin/node /opt/miapi/server.js
Reiniciar un servicio fallido
sudo systemctl reset-failed miapi.service
sudo systemctl start miapi.service
Verificar exposicion de seguridad
systemd-analyze security miapi.service
systemd-analyze verify /etc/systemd/system/miapi.service
Resumen
- Los archivos unit viven en
/etc/systemd/system/y tienen tres secciones:[Unit],[Service]e[Install] - Ejecuta siempre
systemctl daemon-reloaddespues de editar un archivo unit - Usa
Type=simplepara la mayoria de apps modernas; usaType=notifypara apps que senalan disponibilidad - Guarda los secretos en un
EnvironmentFile=separado con permisos600, no directamente en el archivo unit - Configura politicas de reinicio con
Restart=on-failureyStartLimitBurstpara sobrevivir fallos transitorios - Aplica directivas de seguridad —
NoNewPrivileges,ProtectSystem,PrivateTmp— a cada servicio en produccion - Reemplaza los cron jobs con unidades de temporizador para mejor registro de logs y comportamiento de recuperacion
- Depura con journalctl —
journalctl -u nombre-servicio -fes tu primer recurso ante cualquier fallo