← Back to Blog
🏠 Mi Home Lab DevOps
November 25, 2025

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 servicio
  • After: Inicia después de estos servicios
  • Wants: 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
⚠️ Testing: Siempre testa las opciones de security en dev antes de producción. Algunas pueden romper tu servicio si el script necesita acceso a recursos protegidos.

💡 Tips y Mejores Prácticas

  • Usa rutas absolutas: /usr/bin/python3 no python3
  • Run as non-root: Especifica User y Group
  • Usa Type=simple: Más fácil de debuggear que forking
  • Agrega Description descriptivo: Te ayuda a recordar qué hace
  • Logs a journal: StandardOutput=journal centraliza logs
  • RestartSec razonable: 30-60s evita restart loops agresivos
  • Test antes de enable: systemctl start primero, luego enable

📊 Resultado en Mi Servidor

Antes (script + cron):
• 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.