Systemd: más allá del systemctl start
La mayoría de desarrolladores usa systemd para tres cosas: systemctl start, systemctl stop y systemctl restart. Pero systemd es una herramienta enormemente potente que puede hacer la vida de un sysadmin mucho más fácil si le dedicas media hora a entender sus opciones. Este post cubre las funcionalidades que uso a diario y que van mucho más allá de arrancar y parar servicios.
Hardening: que tu servicio no pueda hacer lo que no debe

La funcionalidad de systemd que más me gusta y que menos gente conoce es el hardening de servicios. Con unas pocas directivas puedes limitar lo que un servicio puede hacer en el sistema:
[Service]
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/app/data
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectSystem=strict monta todo el filesystem como solo lectura excepto los paths que declares en ReadWritePaths. Si tu aplicación tiene una vulnerabilidad que permite escribir ficheros, no podrá escribir fuera de /opt/app/data. ProtectHome=true hace que /home sea inaccesible. PrivateTmp le da al servicio su propio /tmp aislado.
Es defensa en profundidad: incluso si tu aplicación se compromete, el daño que puede hacer está contenido. No sustituye a un buen código, pero añade una capa de protección que no cuesta nada activar.
Para auditar el hardening de un servicio existente:
systemd-analyze security mi-servicio.service
Te da una puntuación y una lista de recomendaciones. Apunta al verde.
Dependencias y orden de arranque
Systemd no arranca los servicios en orden secuencial como hacía SysVinit. Los arranca en paralelo, tan rápido como puede. Si tu servicio necesita que la red esté disponible antes de arrancar, tienes que decírselo:
[Unit]
After=network-online.target
Wants=network-online.target
After dice "arranca después de esto". Wants dice "intenta arrancar esto también, pero no falles si no puedes". Si necesitas que sea obligatorio, usa Requires en vez de Wants.
Para dependencias entre tus propios servicios:
[Unit]
After=postgresql.service
Requires=postgresql.service
Esto garantiza que PostgreSQL esté arrancado antes de que tu aplicación intente conectarse. Sin esta directiva, tu app puede arrancar antes que la base de datos y fallar con un "connection refused" que te hace perder media hora diagnosticando.
Restart automático con backoff

La directiva Restart controla qué pasa cuando tu servicio se cae:
[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
Restart=on-failure reinicia el servicio si sale con un código de error (pero no si se para con systemctl stop). RestartSec=5 espera cinco segundos antes de reiniciar para no martillear un servicio que falla inmediatamente. StartLimitBurst=5 y StartLimitIntervalSec=300 limitan a cinco reintentos en cinco minutos para evitar bucles infinitos.
Para servicios críticos puedes usar Restart=always, pero úsalo con cuidado. Si tu servicio falla por una razón legítima (configuración incorrecta, puerto ocupado), reiniciarlo en bucle no va a solucionar nada y sí va a llenar los logs.
Timers: el reemplazo de cron
Los timers de systemd son cron con esteroides. Son más legibles, tienen logging integrado y pueden depender de otros servicios:
# /etc/systemd/system/backup.timer
[Unit]
Description=Backup diario
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Ejecutar backup
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
Persistent=true es clave: si el servidor estaba apagado a las 3:00, ejecuta el backup al encender. Cron simplemente se lo salta.
Para listar los timers activos:
systemctl list-timers --all
Journal: logs estructurados

journalctl es mucho más potente que tail -f /var/log/syslog:
# Logs de un servicio específico
journalctl -u mi-servicio
# Logs desde el último arranque
journalctl -u mi-servicio -b
# Logs de las últimas 2 horas
journalctl -u mi-servicio --since "2 hours ago"
# Seguir en tiempo real
journalctl -u mi-servicio -f
# Solo errores
journalctl -u mi-servicio -p err
# Formato JSON (para procesar con jq)
journalctl -u mi-servicio -o json | jq '.MESSAGE'
# Espacio usado por los logs
journalctl --disk-usage
# Limpiar logs de más de 7 días
journalctl --vacuum-time=7d
Lo que más me gusta del journal es que está integrado con systemd. No necesitas configurar logrotate por separado, no necesitas redirigir stdout a un fichero, no necesitas un servicio de logging aparte. Tu aplicación escribe a stdout y systemd se encarga del resto.
Variables de entorno seguras
En vez de hardcodear configuración en el unit file, usa EnvironmentFile:
[Service]
EnvironmentFile=/opt/app/.env
El fichero .env contiene los secretos:
DATABASE_URL=postgres://user:pass@localhost/mydb
API_TOKEN=secreto
Importante: el fichero .env debe tener permisos restrictivos (chmod 640) y ser propiedad del usuario del servicio. Systemd lo lee al arrancar y lo pasa como variables de entorno al proceso.
No uses Environment= directamente en el unit file para secretos, porque el unit file suele estar en /etc/systemd/system/ con permisos legibles para todos.
Watchdog
El watchdog de systemd mata y reinicia tu servicio si deja de responder. Tu aplicación tiene que enviar un heartbeat periódico:
[Service]
Type=notify
WatchdogSec=30
En Go, puedes usar el paquete go-systemd para enviar el heartbeat:
// Enviar heartbeat cada 15 segundos (la mitad de WatchdogSec)
go func() {
for range time.Tick(15 * time.Second) {
daemon.SdNotify(false, daemon.SdNotifyWatchdog)
}
}()
Si tu aplicación se queda bloqueada (deadlock, goroutine leak, consumo de memoria descontrolado), el watchdog lo detecta y reinicia el servicio. Es un seguro contra los fallos silenciosos que no producen un crash pero dejan la aplicación inservible.
ExecStartPre y ExecStartPost
Puedes ejecutar comandos antes y después de arrancar el servicio:
[Service]
ExecStartPre=/usr/bin/test -f /opt/app/config.yaml
ExecStartPre=+/usr/local/go/bin/go build -o /opt/app/bin/server ./cmd/server
ExecStart=/opt/app/bin/server
ExecStartPost=/usr/bin/curl -s http://localhost:8080/health
El prefijo + ejecuta el comando como root, independientemente del User del servicio. Es útil para compilar binarios o crear directorios que necesitan permisos elevados.
Si algún ExecStartPre falla, el servicio no arranca. Esto es perfecto para validaciones: comprobar que un fichero de configuración existe, que un puerto está libre, o que una dependencia externa responde.
Conclusión
Systemd tiene mala fama entre ciertos sectores de la comunidad Linux. Parte de esa mala fama es merecida: es complejo, monolítico y hace demasiadas cosas. Pero si lo aceptas como lo que es y aprendes a usarlo bien, es una herramienta extraordinariamente potente para gestionar servicios en producción.
El hardening por sí solo justifica aprender systemd en profundidad. La capacidad de decir "este servicio solo puede escribir en este directorio" con una línea de configuración es algo que antes requería contenedores, chroot o SELinux. Ahora es un ProtectSystem=strict.