Gestionar servicios en distribuciones Linux modernas significa trabajar con systemd, el sistema de inicio y gestor de servicios que se ha convertido en el estándar en la mayoría de distribuciones. Esta guía cubre todo lo que necesitas saber sobre crear unidades de servicio personalizadas, gestionar dependencias entre servicios, analizar logs con journalctl, reemplazar cron con timers de systemd y diagnosticar problemas cuando los servicios no arrancan o se detienen inesperadamente.

Requisitos Previos

  • Una distribución Linux que ejecute systemd (Ubuntu 16.04+, CentOS 7+, Debian 8+, Fedora 15+)
  • Acceso a terminal con privilegios sudo
  • Comprensión básica de permisos de archivos Linux y gestión de procesos
  • Un editor de texto instalado (vim, nano o similar)

Entendiendo las Unidades de Systemd

Systemd organiza todo en unidades — recursos que el sistema sabe cómo gestionar. Los tipos de unidad más comunes son:

Tipo de UnidadExtensiónPropósito
Service.serviceProcesos daemon y tareas únicas
Timer.timerEjecución programada (reemplaza cron)
Socket.socketActivación de sockets IPC y red
Mount.mountPuntos de montaje del sistema de archivos
Target.targetGrupos de unidades (como runlevels)

Los archivos de unidad residen en tres ubicaciones, con prioridad descendente:

/etc/systemd/system/      # Unidades personalizadas del admin (mayor prioridad)
/run/systemd/system/       # Unidades en tiempo de ejecución
/lib/systemd/system/       # Unidades predeterminadas de la distribución

Para listar todas las unidades cargadas y sus estados:

systemctl list-units --type=service
systemctl list-units --type=service --state=failed

Creación de Unidades de Servicio Personalizadas

Un archivo de unidad de servicio bien estructurado tiene tres secciones. Aquí hay un ejemplo completo para una aplicación Node.js:

# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
Documentation=https://example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStartPre=/usr/bin/node --check /opt/myapp/server.js
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Endurecimiento de seguridad
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Directivas clave en la sección [Service]:

  • Type: simple (predeterminado, proceso en primer plano), forking (para daemons que hacen fork), oneshot (se ejecuta una vez y termina), notify (señala disponibilidad vía sd_notify)
  • ExecStartPre: Comando para ejecutar validación antes de iniciar el proceso principal
  • Restart: on-failure, always, on-abnormal o no
  • RestartSec: Retardo entre intentos de reinicio en segundos

Después de crear el archivo, cárgalo e inícialo:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service

Gestión de Dependencias

Systemd usa varias directivas para expresar relaciones entre unidades:

[Unit]
# Ordenamiento (cuándo iniciar, no si debe hacerlo)
After=network-online.target    # Iniciar después de que la red esté lista
Before=nginx.service           # Iniciar antes de nginx

# Fuerza de dependencia
Requires=postgresql.service    # Dependencia fuerte — falla si postgres falla
Wants=redis.service            # Dependencia suave — continúa si redis falla
BindsTo=docker.service         # Ciclo de vida vinculado — se detiene cuando docker se detiene

# Resolución de conflictos
Conflicts=iptables.service     # No puede ejecutarse junto con iptables

La diferencia entre After y Requires es crítica. After controla el ordenamiento (secuencia) mientras que Requires controla la activación (si se debe incorporar una dependencia). Normalmente necesitas ambos:

Requires=postgresql.service
After=postgresql.service

Usar Requires solo no garantiza el orden — ambos servicios pueden iniciar simultáneamente. Usar After solo no incorpora la dependencia — solo los ordena si ambos están iniciando.

Para visualizar el árbol de dependencias de un servicio:

systemctl list-dependencies myapp.service
systemctl list-dependencies myapp.service --reverse

Análisis de Logs con Journalctl

Journalctl es la herramienta para consultar el journal de systemd. Estos son los patrones más útiles:

# Seguir logs de un servicio específico en tiempo real
journalctl -u myapp.service -f

# Mostrar logs desde el último arranque
journalctl -u myapp.service -b

# Filtrar por rango de tiempo
journalctl -u myapp.service --since "2026-02-20 08:00" --until "2026-02-20 18:00"

# Mostrar solo errores y superiores
journalctl -u myapp.service -p err

# Salida en formato JSON para procesamiento
journalctl -u myapp.service -o json-pretty --no-pager

# Mostrar mensajes del kernel relacionados con OOM kills
journalctl -k --grep="Out of memory"

# Verificar uso de disco del journal
journalctl --disk-usage

# Limpiar logs antiguos para liberar espacio
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-size=500M

Los niveles de prioridad siguen la convención syslog: emerg (0), alert (1), crit (2), err (3), warning (4), notice (5), info (6), debug (7). Usar -p err muestra err y todo lo más severo.

Para configurar almacenamiento persistente del journal (sobrevive reinicios):

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

Timers de Systemd

Los timers de systemd reemplazan cron con mejor registro, gestión de dependencias y fiabilidad. Un timer consiste en dos archivos — el .timer y su .service emparejado:

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily database backup timer

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=900
Persistent=true
Unit=backup.service

[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup job

[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal

Expresiones comunes de OnCalendar:

ExpresiónSignificado
*-*-* 02:00:00Diariamente a las 2 AM
Mon *-*-* 09:00:00Cada lunes a las 9 AM
*-*-01 00:00:00Primer día de cada mes
*-*-* *:00/15:00Cada 15 minutos
hourlyCada hora (abreviación)

Habilitar y gestionar timers:

sudo systemctl enable --now backup.timer
systemctl list-timers --all
systemctl status backup.timer

La directiva Persistent=true asegura que si el sistema estaba apagado cuando el timer debía dispararse, ejecuta el trabajo inmediatamente en el siguiente arranque.

Comparativa: Systemd vs SysVinit vs Upstart

CaracterísticasystemdSysVinitUpstart
Inicio paraleloNoParcial
Gestión de dependenciasDirectivas declarativasOrdenamiento manual con númerosBasado en eventos
Supervisión de serviciosPolíticas de reinicio integradasNinguna (requiere herramientas externas)Directiva respawn
Registrojournald (estructurado, indexado)syslog (archivos de texto plano)syslog
Activación por socketNoNo
Control de recursosIntegración con cgroupsNingunoNinguno
Temporizadores/programaciónTimers integradosRequiere cronRequiere cron
Formato de configuraciónArchivos de unidad estilo INIScripts de shellArchivos conf basados en stanzas
Análisis de arranquesystemd-analyzeNingunoNinguno
Estado de adopciónPredeterminado en la mayoría de distrosSistemas legacyDescontinuado

Escenario del Mundo Real

Tienes un servidor de producción ejecutando una API en Python que depende de PostgreSQL y Redis. La API debe iniciar después de que ambas bases de datos estén listas, reiniciarse en caso de fallos con una estrategia de back-off, y ejecutar un trabajo de limpieza cada 6 horas.

Unidad de servicio (/etc/systemd/system/api.service):

[Unit]
Description=Production Python API
After=network-online.target postgresql.service redis.service
Requires=postgresql.service
Wants=redis.service

[Service]
Type=simple
User=apiuser
WorkingDirectory=/opt/api
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/api/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 app:create_app()
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=5

[Install]
WantedBy=multi-user.target

Timer de limpieza (/etc/systemd/system/api-cleanup.timer):

[Unit]
Description=API cleanup every 6 hours

[Timer]
OnBootSec=15min
OnUnitActiveSec=6h
Persistent=true

[Install]
WantedBy=timers.target

Esta configuración asegura que la API solo inicia cuando PostgreSQL está ejecutándose (dependencia fuerte) y opcionalmente usa Redis (dependencia suave). La política de reinicio permite 5 intentos de reinicio dentro de 5 minutos antes de desistir.

Errores Comunes y Casos Especiales

  • ExecStart debe usar rutas absolutas: Las rutas relativas causan fallos silenciosos. Siempre usa /usr/bin/node, no node
  • Type=forking necesita PIDFile: Si tu daemon hace fork, systemd debe rastrear el PID hijo. Establece PIDFile=/run/myapp.pid y asegúrate de que tu app lo escriba
  • Archivos Environment vs inline: Para muchas variables, usa EnvironmentFile=/etc/myapp/env en lugar de múltiples líneas Environment=. El formato es KEY=value (sin export)
  • Servicios de usuario vs servicios del sistema: Las unidades de usuario en ~/.config/systemd/user/ se ejecutan sin sudo pero solo mientras el usuario está conectado (a menos que se establezca loginctl enable-linger)
  • Reload vs restart vs daemon-reload: systemctl restart myapp reinicia el proceso. systemctl daemon-reload recarga las definiciones de archivos de unidad. Después de editar un archivo de unidad necesitas daemon-reload primero
  • El target de WantedBy importa: Usa multi-user.target para servidores sin interfaz gráfica, graphical.target para servicios de escritorio

Solución de Problemas

Cuando un servicio no arranca, sigue este flujo de diagnóstico:

# 1. Verificar estado actual y logs recientes
systemctl status myapp.service

# 2. Obtener información detallada del fallo
journalctl -u myapp.service -e --no-pager -n 50

# 3. Validar sintaxis del archivo de unidad
systemd-analyze verify /etc/systemd/system/myapp.service

# 4. Verificar ciclos de ordenamiento
systemd-analyze critical-chain myapp.service

# 5. Probar el comando ExecStart manualmente como el usuario del servicio
sudo -u appuser /usr/bin/node /opt/myapp/server.js

# 6. Verificar denegaciones de SELinux o AppArmor
sudo ausearch -m avc -ts recent 2>/dev/null || sudo journalctl -k --grep="apparmor"

# 7. Verificar permisos de archivos
ls -la /opt/myapp/
namei -l /opt/myapp/server.js

Razones comunes de fallo y soluciones:

SíntomaCausaSolución
code=exited, status=203/EXECBinario no encontrado o no ejecutableVerifica la ruta con which, establece chmod +x
code=exited, status=217/USEREl usuario especificado en User= no existeCrea el usuario con useradd -r -s /sbin/nologin
Start request repeated too quicklyBucle de reinicio alcanzó StartLimitBurstAumenta RestartSec, corrige el fallo subyacente, luego systemctl reset-failed
code=exited, status=200/CHDIRWorkingDirectory no existeCrea el directorio y corrige la propiedad
Servicio inicia pero puerto no accesibleFirewall o dirección de enlace incorrectaVerifica con ss -tlnp, revisa reglas de firewall-cmd o ufw

Resumen

  • Los archivos de unidad de systemd usan secciones declarativas estilo INI ([Unit], [Service], [Install]) para definir el comportamiento del servicio
  • Las unidades personalizadas van en /etc/systemd/system/ — siempre ejecuta daemon-reload después de cambios
  • Usa Requires= junto con After= para dependencias fuertes con ordenamiento correcto
  • Journalctl proporciona filtrado potente por unidad, prioridad, rango de tiempo y formato de salida
  • Los timers de systemd ofrecen mejor fiabilidad que cron con programación persistente y registro por trabajo
  • Las directivas de endurecimiento de seguridad como ProtectSystem=strict y PrivateTmp=true deberían ser estándar en unidades de producción
  • Diagnostica con systemctl status, journalctl -u y systemd-analyze verify

Artículos Relacionados