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 Unidad | Extensión | Propósito |
|---|---|---|
| Service | .service | Procesos daemon y tareas únicas |
| Timer | .timer | Ejecución programada (reemplaza cron) |
| Socket | .socket | Activación de sockets IPC y red |
| Mount | .mount | Puntos de montaje del sistema de archivos |
| Target | .target | Grupos 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-abnormalono - 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ón | Significado |
|---|---|
*-*-* 02:00:00 | Diariamente a las 2 AM |
Mon *-*-* 09:00:00 | Cada lunes a las 9 AM |
*-*-01 00:00:00 | Primer día de cada mes |
*-*-* *:00/15:00 | Cada 15 minutos |
hourly | Cada 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ística | systemd | SysVinit | Upstart |
|---|---|---|---|
| Inicio paralelo | Sí | No | Parcial |
| Gestión de dependencias | Directivas declarativas | Ordenamiento manual con números | Basado en eventos |
| Supervisión de servicios | Políticas de reinicio integradas | Ninguna (requiere herramientas externas) | Directiva respawn |
| Registro | journald (estructurado, indexado) | syslog (archivos de texto plano) | syslog |
| Activación por socket | Sí | No | No |
| Control de recursos | Integración con cgroups | Ninguno | Ninguno |
| Temporizadores/programación | Timers integrados | Requiere cron | Requiere cron |
| Formato de configuración | Archivos de unidad estilo INI | Scripts de shell | Archivos conf basados en stanzas |
| Análisis de arranque | systemd-analyze | Ninguno | Ninguno |
| Estado de adopción | Predeterminado en la mayoría de distros | Sistemas legacy | Descontinuado |
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, nonode - Type=forking necesita PIDFile: Si tu daemon hace fork, systemd debe rastrear el PID hijo. Establece
PIDFile=/run/myapp.pidy asegúrate de que tu app lo escriba - Archivos Environment vs inline: Para muchas variables, usa
EnvironmentFile=/etc/myapp/enven lugar de múltiples líneasEnvironment=. El formato esKEY=value(sinexport) - 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 establezcaloginctl enable-linger) - Reload vs restart vs daemon-reload:
systemctl restart myappreinicia el proceso.systemctl daemon-reloadrecarga las definiciones de archivos de unidad. Después de editar un archivo de unidad necesitasdaemon-reloadprimero - El target de WantedBy importa: Usa
multi-user.targetpara servidores sin interfaz gráfica,graphical.targetpara 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íntoma | Causa | Solución |
|---|---|---|
code=exited, status=203/EXEC | Binario no encontrado o no ejecutable | Verifica la ruta con which, establece chmod +x |
code=exited, status=217/USER | El usuario especificado en User= no existe | Crea el usuario con useradd -r -s /sbin/nologin |
Start request repeated too quickly | Bucle de reinicio alcanzó StartLimitBurst | Aumenta RestartSec, corrige el fallo subyacente, luego systemctl reset-failed |
code=exited, status=200/CHDIR | WorkingDirectory no existe | Crea el directorio y corrige la propiedad |
| Servicio inicia pero puerto no accesible | Firewall o dirección de enlace incorrecta | Verifica 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 ejecutadaemon-reloaddespués de cambios - Usa
Requires=junto conAfter=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=strictyPrivateTmp=truedeberían ser estándar en unidades de producción - Diagnostica con
systemctl status,journalctl -uysystemd-analyze verify