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.

SYSTEMD — GESTION DE SERVICIOS EN LINUX nginx.service active (running) myapp.service active (running) backup.timer active (waiting) worker.service active (running) Gestiona todos los procesos de tu servidor Linux desde un solo sistema init

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 sudo o como root
  • Familiaridad basica con la linea de comandos y un editor de texto (nano o vim)
  • 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]

DirectivaProposito
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]

DirectivaProposito
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]

DirectivaProposito
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

TipoCuando UsarComo systemd Rastrea la Disponibilidad
simpleLa mayoria de apps modernas. ExecStart es el proceso principal.Considera la unidad iniciada tan pronto como ExecStart hace fork
execComo simple, pero espera a que exec tenga exitoEspera hasta que el binario se ejecuta correctamente
forkingDaemons tradicionales que hacen fork y el padre terminaEspera a que el proceso padre termine
oneshotScripts que se ejecutan una vez y terminan (tareas de inicializacion)Espera a que ExecStart termine antes de marcar como activo
notifyApps 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

CaracteristicasystemdSysVinit / init.dsupervisordPM2
Incluido con el SOSi (todas las distros principales)Si (legado)No (pip install)No (npm install)
Orden de dependenciasGrafo completoOrden manualLimitadoNo
Activacion por socketSiNoNoNo
Limites de recursos cgroupSi (nativo)NoNoNo
Sandboxing a nivel kernelSi (extenso)NoNoNo
Logs centralizadosjournald (estructurado)syslog / archivosSolo archivosArchivos PM2
Jobs programados (timers)Si (unidades timer)NoNoSi (similar a cron)
Integracion con Node.jsBuenaPobreBuenaExcelente
Curva de aprendizajeModeradaBajaBajaMuy 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-reload despues de editar un archivo unit
  • Usa Type=simple para la mayoria de apps modernas; usa Type=notify para apps que senalan disponibilidad
  • Guarda los secretos en un EnvironmentFile= separado con permisos 600, no directamente en el archivo unit
  • Configura politicas de reinicio con Restart=on-failure y StartLimitBurst para sobrevivir fallos transitorios
  • Aplica directivas de seguridadNoNewPrivileges, 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 journalctljournalctl -u nombre-servicio -f es tu primer recurso ante cualquier fallo

Articulos Relacionados