Systemd Services: De Script Bash a Daemon Profesional
📋 Contexto
Tenía un script Python (sysmon_web.py) que generaba el status HTML de mi servidor. Lo ejecutaba manualmente o con cron, pero quería algo más robusto: que inicie automáticamente al bootear, se reinicie si falla, y tenga logs centralizados. La solución: convertirlo en un systemd service.
🎯 Objetivo
Convertir un script bash/python en un servicio systemd con:
- ✅ Auto-start al bootear el sistema
- ✅ Auto-restart si el proceso falla
- ✅ Logs centralizados con journalctl
- ✅ Control con systemctl (start/stop/restart/status)
- ✅ Ejecución como usuario no-root
🛠️ Anatomía de un Service File
Ejemplo: sysmon-web.service
[Unit]
Description=SysDevOps Web Monitor - Server Status Generator
Documentation=https://github.com/romerok9/server-monitor
After=network.target docker.service
Wants=docker.service
[Service]
Type=simple
User=mytechzone
Group=mytechzone
WorkingDirectory=/home/mytechzone/scripts
# Main process
ExecStart=/usr/bin/python3 /home/mytechzone/scripts/sysmon_web.py
# Restart policy
Restart=always
RestartSec=30
# Resource limits (optional)
MemoryLimit=256M
CPUQuota=50%
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=sysmon-web
# Security (optional but recommended)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/home/mytechzone/n8n-lab/website/html
[Install]
WantedBy=multi-user.target
Desglose de Secciones
[Unit] - Metadata y Dependencias
Description: Qué hace el servicioAfter: Inicia después de estos serviciosWants: Dependencias opcionales (no críticas)Requires: Dependencias obligatorias
[Service] - Configuración de Ejecución
| Directiva | Propósito |
|---|---|
Type=simple |
Proceso en foreground (default) |
Type=forking |
Proceso hace fork (típico de daemons) |
User/Group |
Usuario para ejecutar el proceso |
WorkingDirectory |
Directorio de trabajo |
ExecStart |
Comando a ejecutar |
ExecStartPre |
Comando antes de iniciar |
ExecStop |
Comando al detener |
Restart=always |
Reinicia siempre que termine |
RestartSec=30 |
Espera 30s antes de reiniciar |
[Install] - Comportamiento de Instalación
WantedBy=multi-user.target: Inicia en runlevel multi-usuario (equivalente a runlevel 3)WantedBy=graphical.target: Inicia en modo gráfico (runlevel 5)
🚀 Proceso Completo: Script a Service
Paso 1: Crear el Service File
# Local machine
cat > sysmon-web.service << 'EOF'
[Unit]
Description=SysDevOps Web Monitor
After=network.target
[Service]
User=mytechzone
Group=mytechzone
WorkingDirectory=/home/mytechzone/scripts
ExecStart=/usr/bin/python3 /home/mytechzone/scripts/sysmon_web.py
Restart=always
RestartSec=30
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
Paso 2: Copiar al Servidor
scp sysmon-web.service mytechzone:/tmp/
Paso 3: Instalar el Service
# On server
sudo mv /tmp/sysmon-web.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/sysmon-web.service
# Reload systemd para detectar el nuevo service
sudo systemctl daemon-reload
# Enable (auto-start al bootear)
sudo systemctl enable sysmon-web.service
# Start now
sudo systemctl start sysmon-web.service
Paso 4: Verificar Estado
# Check status
sudo systemctl status sysmon-web
# Check logs
sudo journalctl -u sysmon-web -f
# Check if enabled
sudo systemctl is-enabled sysmon-web
💡 Ejemplos Prácticos
Ejemplo 1: Script de Backup con Pre/Post Commands
[Unit]
Description=Daily Backup Service
After=network.target
[Service]
Type=oneshot
User=backupuser
WorkingDirectory=/opt/backups
# Pre-check: Ensure target directory exists
ExecStartPre=/bin/mkdir -p /opt/backups/daily
# Main backup
ExecStart=/opt/scripts/backup.sh
# Post-backup: Send notification
ExecStartPost=/opt/scripts/notify-backup-complete.sh
# Cleanup old backups
ExecStartPost=/usr/bin/find /opt/backups/daily -mtime +7 -delete
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Programar con timer:
# backup.timer
[Unit]
Description=Daily Backup Timer
[Timer]
OnCalendar=daily
OnCalendar=02:00
Persistent=true
[Install]
WantedBy=timers.target
# Enable timer
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
Ejemplo 2: Service con Environment Variables
[Unit]
Description=My API Service
After=network.target
[Service]
Type=simple
User=apiuser
WorkingDirectory=/opt/api
# Environment variables
Environment="NODE_ENV=production"
Environment="PORT=3000"
Environment="LOG_LEVEL=info"
# Or load from file
EnvironmentFile=/opt/api/.env
ExecStart=/usr/bin/node /opt/api/server.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Ejemplo 3: Service con Healthcheck
[Unit]
Description=Web Service with Healthcheck
After=network.target
[Service]
Type=simple
User=webuser
WorkingDirectory=/opt/web
ExecStart=/usr/bin/python3 /opt/web/app.py
# Healthcheck every 30s
ExecStartPost=/bin/sleep 5
ExecStartPost=/usr/bin/curl -f http://localhost:5000/health || exit 1
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.target
🔍 Comandos systemctl Esenciales
# Control básico
sudo systemctl start myservice
sudo systemctl stop myservice
sudo systemctl restart myservice
sudo systemctl reload myservice # Sin interrumpir conexiones
# Estado
sudo systemctl status myservice
sudo systemctl is-active myservice
sudo systemctl is-enabled myservice
sudo systemctl is-failed myservice
# Enable/Disable auto-start
sudo systemctl enable myservice
sudo systemctl disable myservice
# Logs
sudo journalctl -u myservice # All logs
sudo journalctl -u myservice -f # Follow (tail -f)
sudo journalctl -u myservice -n 50 # Last 50 lines
sudo journalctl -u myservice --since "1 hour ago"
sudo journalctl -u myservice --since "2024-12-01"
# List all services
systemctl list-units --type=service
systemctl list-units --type=service --state=running
systemctl list-units --type=service --state=failed
# Reload systemd after editing service files
sudo systemctl daemon-reload
🐛 Troubleshooting
Service No Inicia
# Check syntax errors
sudo systemd-analyze verify /etc/systemd/system/myservice.service
# Check status and logs
sudo systemctl status myservice
sudo journalctl -u myservice -n 100 --no-pager
# Test ExecStart command manually
sudo -u myuser /usr/bin/python3 /path/to/script.py
# Check file permissions
ls -la /etc/systemd/system/myservice.service
ls -la /path/to/script.py
Service Falla Constantemente
# Check restart loop
sudo journalctl -u myservice | grep -i "restart"
# Increase RestartSec to avoid rapid restart loops
RestartSec=60
# Or limit restart attempts
StartLimitInterval=300
StartLimitBurst=5
Logs No Aparecen en journalctl
# Ensure output goes to journal
StandardOutput=journal
StandardError=journal
# If script uses logging to file, redirect:
ExecStart=/bin/bash -c '/path/to/script.sh 2>&1 | /usr/bin/logger -t myservice'
🔒 Security Hardening
[Service]
# Basic security
NoNewPrivileges=true
PrivateTmp=true
# Filesystem protection
ProtectSystem=strict # Read-only /usr, /boot, /efi
ProtectHome=true # Hide /home
ReadWritePaths=/var/myapp # Only allow writes here
# Kernel protection
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
# Restrict capabilities
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# Network isolation (if not needed)
PrivateNetwork=true
# Restrict system calls
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
💡 Tips y Mejores Prácticas
- Usa rutas absolutas:
/usr/bin/python3nopython3 - Run as non-root: Especifica
UseryGroup - Usa
Type=simple: Más fácil de debuggear queforking - Agrega
Descriptiondescriptivo: Te ayuda a recordar qué hace - Logs a journal:
StandardOutput=journalcentraliza logs - RestartSec razonable: 30-60s evita restart loops agresivos
- Test antes de enable:
systemctl startprimero, luegoenable
📊 Resultado en Mi Servidor
• Ejecutaba cada 5 min con cron
• Si fallaba, esperaba al próximo cron
• Logs dispersos en archivos
• No auto-start al bootear (tenía que configurar cron @reboot)
Después (systemd service):
✓ Corre continuamente (actualiza cada 30s)
✓ Auto-restart si falla (en 30s)
✓ Logs centralizados con
journalctl✓ Auto-start al bootear
✓ Control con
systemctl✓ Uptime visible:
systemctl status sysmon-web
📚 Recursos
💭 Conclusión
Convertir scripts en systemd services es el paso de "funciona en mi terminal" a "production-ready". Te da confiabilidad, observabilidad y control profesional sobre tus servicios. Si tienes un script que corre en cron y es crítico para tu infra, considera convertirlo a systemd service.
En mi caso, transformó un script de monitoreo de "esperemos que cron lo ejecute" a un daemon robusto que nunca he tenido que tocar en meses.