Hasta ahora has construido APIs REST, las has metido en contenedores Docker y les has puesto healthchecks y métricas. Tu código funciona, está observable y se despliega con un docker compose up -d.
Pero hay servicios que no necesitan HTTP. No necesitan un puerto abierto ni un balanceador delante. Necesitan ejecutarse en segundo plano, en la máquina host, vigilando el sistema, rotando logs, sincronizando archivos, o haciendo cualquier tarea periódica que antes hacías con cron.
Si vienes de Bash, piensa en esto como pasar de:
# Script que ejecutas a mano o con crontab
*/5 * * * * /home/usuario/check-disk.sh >> /var/log/checks.log
# Y tienes que acordarte de mirar el log
tail -f /var/log/checks.log
A esto:
# Daemon systemd que arranca solo, escribe a journald y se reinicia si falla
sudo systemctl start crustaceo-monitor
sudo journalctl -u crustaceo-monitor -f
En este capítulo construyes crustaceo-monitor, un daemon de monitoreo escrito en Rust que se instala como servicio systemd, escribe alertas a journald, responde a señales para graceful shutdown y checks inmediatos, y se configura con un archivo TOML.
¿Por qué systemd y no Docker para esto?
Antes de escribir una línea de código, tienes que entender cuándo usas Docker y cuándo usas systemd. No son excluyentes: se complementan.
| Criterio | Docker | systemd |
|---|---|---|
| Aislamiento | Completo (namespaces, cgroups) | Parcial (hardening del unit) |
| Acceso al hardware | Limitado (hay que exponer) | Directo |
| Dependencia del host | Mínima (solo kernel) | Total (systemd, journald, red local) |
| Arranque en boot | Docker arranca después | systemd arranca en orden |
| Logging | docker logs → stdout | journalctl → journald |
| Monitorización del proceso | Healthchecks HTTP | Systemd watchdog, señales UNIX |
| Complejidad | Media-alta (orquestación) | Baja (unit file) |
Para un daemon de monitoreo del sistema, la respuesta es clara: systemd. Necesitas acceso a /proc, /sys, a los discos montados, a los procesos del sistema. Meter eso en un contenedor añade complejidad sin beneficio real.
Máxima de atareao.es: «Docker para aislar servicios que sirven red. systemd para servicios que tocan el sistema.»
Dicho esto, si quieres que crustaceo-monitor se ejecute dentro de un contenedor —por ejemplo en un clúster de Kubernetes—, puedes hacerlo. Pero el escenario natural de este capítulo es un servidor Linux con systemd, como cualquier VPS de tu infraestructura self-hosted.
Daemonización: el mito del fork
Si vienes de Bash o de C clásico, igual piensas que un daemon necesita hacer fork(), que el proceso hijo herede el PID 1, y que el padre muera. Eso se llamaba «daemonización a la antigua» y se hacía así:
# Pseudocódigo de daemonización clásica
fork() # Crear hijo
if padre: exit() # El padre muere
setsid() # Nueva sesión, nuevo grupo de procesos
fork() again # Segundo fork para asegurar que no retomamos terminal
chdir("/") # No bloquear ningún mount point
umask(0) # No heredar máscara del shell
close(stdin/stdout/stderr) # Cerrar herederos de la terminal
Todo eso era necesario porque los daemon se lanzaban desde un terminal y tenían que desvincularse para no morir cuando el usuario cerraba la sesión.
Con systemd, todo eso sobra.
Cuando systemd ejecuta tu servicio:
- Tu proceso se lanza en su propio cgroup, aislado de la terminal de usuario.
- systemd captura stdout y lo redirige a journald automáticamente.
- Si pones
Type=simple(el valor por defecto), systemd considera que el servicio está «listo» en cuanto se ejecuta el binario. - Si tu proceso muere, systemd lo reinicia según la directiva
Restart=.
No necesitas fork, no necesitas setsid, no necesitas cerrar fds. Escribes un programa normal que imprime a stdout y systemd se encarga del resto.
Tu main() en Rust es un fn main() normal, o un async fn main() con tokio. Nada especial. Sin magia de daemonización.
Logging a journald: stdout es suficiente
Una de las preguntas más frecuentes es: «¿necesito la crate journald para escribir al log del sistema?».
La respuesta corta es: no, si usas systemd.
Cuando un servicio systemd se ejecuta con Type=simple, systemd captura todo lo que el proceso escribe a stdout y lo inyecta en journald. Cada línea se convierte en una entrada del log con metadatos automáticos: PID, unit, timestamp, prioridad.
Esto significa que tu println!() en Rust ya es logging a journald. No necesitas librerías especiales.
Pero hay matices:
- Prioridad: Por defecto, systemd asigna prioridad
infoa stdout yerra stderr. Puedes controlarlo conStandardOutput=journalyStandardError=journalen el unit file. - Estructura: Cada
println!es una entrada separada. Si quieres logs multi-línea como una sola entrada, usaeprintln!. - Formato: Systemd parsea automáticamente logs estructurados si usas el formato
KEY=VALUE. Pero para este tutorial, texto plano es más que suficiente.
Si en el futuro necesitas enviar logs estructurados con campos personalizados (como CHECK_NAME=disk, STATUS=CRITICAL), la crate journald te permite escribir directamente al socket de journald con metadatos arbitrarios. Pero para el 90% de los casos, println! basta.
// Esto ya es logging a journald cuando corres bajo systemd
println!("[2026-06-24 10:00:00] ALERT | disk | Disco al 95%");
// Si quieres prioridad de error, usa stderr
eprintln!("[2026-06-24 10:00:00] CRITICAL | memory | Memoria agotada");
Manejo de señales: graceful shutdown y SIGUSR1
Un daemon bien educado responde a las señales del sistema. Cuando el administrador ejecuta systemctl stop, systemd envía SIGTERM al proceso. Si tu daemon no lo captura, el kernel lo mata con SIGKILL a los pocos segundos y no tienes oportunidad de cerrar recursos, escribir un estado final o notificar a otros procesos.
Las señales que te importan en este proyecto:
| Señal | Propósito | Acción en crustaceo-monitor |
|---|---|---|
| SIGTERM | Apagado ordenado desde systemd | Activar flag SHUTDOWN, salir del bucle, terminar con 0 |
| SIGINT | Ctrl+C desde terminal | Igual que SIGTERM (para pruebas en desarrollo) |
| SIGHUP | Recarga tradicional de daemons | Forzar comprobación inmediata, como si fuera un nuevo ciclo |
| SIGUSR1 | Señal de usuario definida por el programador | Forzar comprobación inmediata sin esperar al intervalo |
Cómo funciona en el código
Usas tokio::signal::unix que es la forma canónica de manejar señales en una aplicación tokio asíncrona. No necesitas la crate signal-hook porque tokio ya incluye soporte para señales UNIX en su módulo tokio::signal::unix.
El patrón es simple pero efectivo:
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigusr1 = signal(SignalKind::user_defined1())?;
tokio::spawn(async move {
loop {
tokio::select! {
_ = sigterm.recv() => {
SHUTDOWN.store(true, Ordering::Relaxed);
break;
}
_ = sigusr1.recv() => {
FORCE_CHECK.store(true, Ordering::Relaxed);
}
}
}
});
Cada SignalKind crea un receptor. Cuando la señal llega, el select! despierta la rama correspondiente. No hay polling, no hay CPU perdida — tokio usa signalfd en Linux internamente, que es un mecanismo eficiente del kernel.
El truco de los flags atómicos
En lugar de usar canales (que complican el código), usamos dos AtomicBool globales. Son estáticos, no necesitan referencia compartida, y su acceso es prácticamente gratuito en términos de rendimiento.
static SHUTDOWN: AtomicBool = AtomicBool::new(false);
static FORCE_CHECK: AtomicBool = AtomicBool::new(false);
El bucle principal del daemon itera cada segundo (no espera el intervalo entero) precisamente para poder reaccionar rápido a estas señales:
for _ in 0..interval {
if SHUTDOWN.load(Ordering::Relaxed) {
break; // Salir ordenadamente
}
if FORCE_CHECK.swap(false, Ordering::Relaxed) {
run_checks(&config, &mut sys); // Check inmediato
}
sleep(Duration::from_secs(1)).await;
}
El Ordering::Relaxed es suficiente aquí porque la única sincronización que necesitamos es que el valor se propague entre hilos en algún momento. No hay operaciones complejas de lectura-modificación-escritura que requieran ordenar memoria.
Probar las señales manualmente
Cuando el daemon está corriendo, puedes probar las señales desde otra terminal:
# Obtener el PID del daemon
PID=$(systemctl show -p MainPID --value crustaceo-monitor)
echo $PID
# Enviar SIGUSR1 para check inmediato
kill -SIGUSR1 $PID
# Ver la respuesta en los logs
journalctl -u crustaceo-monitor -n 3
# Deberías ver: ⚡ Recibida SIGUSR1 — forzando comprobación inmediata
Esta capacidad de reaccionar a SIGUSR1 es extremadamente útil en entornos de producción: puedes forzar una comprobación sin tener que cambiar la configuración, sin reiniciar el servicio, y sin esperar al siguiente intervalo. Por ejemplo, si sospechas que un servicio acaba de caer, envías SIGUSR1 y ves el resultado en los logs al instante.
Variables de entorno desde el unit file
Systemd permite inyectar variables de entorno en el servicio de dos formas:
Environment=
Directiva directa en el unit file. Útil para valores cortos y fijos.
[Service]
Environment=RUST_BACKTRACE=1
Environment=RUST_LOG=crustaceo_monitor=info
EnvironmentFile=
Apuntar a un archivo con pares CLAVE=valor. Útil para secretos o configuraciones que no quieres en el repositorio.
[Service]
EnvironmentFile=/etc/crustaceo-monitor/env
El archivo /etc/crustaceo-monitor/env contiene:
RUST_BACKTRACE=1
RUST_LOG=crustaceo_monitor=debug
Tu programa Rust accede a estas variables con std::env::var("RUST_LOG") como siempre.
Hardening del servicio
Systemd tiene un sistema de hardening que te permite restringir lo que el servicio puede hacer. Es una capa de seguridad adicional que complementa los permisos de usuario y las capabilities de Linux.
En el unit file de crustaceo-monitor aplicamos estas directivas:
[Service]
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_SYS_PTRACE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
LockPersonality=true
RemoveIPC=true
PrivateDevices=true
Cada una hace lo siguiente:
- CapabilityBoundingSet: Limita las capabilities del kernel que el proceso puede usar. Solo permitimos las mínimas:
CAP_DAC_OVERRIDE(acceso a archivos del sistema para leer /etc/crustaceo-monitor/config.toml) yCAP_SYS_PTRACE(lectura de /proc para métricas de CPU y procesos con sysinfo). Si no necesitas métricas de procesos de otros usuarios, puedes quitar la segunda. - NoNewPrivileges: Impide que el proceso o sus hijos escalen privilegios vía sudo, setuid, etc. Esto es crítico: incluso si el binario tuviera una vulnerabilidad que intente ejecutar
sudo, systemd lo bloquea. - PrivateTmp: Cada ejecución tiene su propio /tmp montado como bind mount privado. No comparte archivos temporales con otros procesos del sistema, eliminando una clase completa de ataques (race conditions en /tmp).
- ProtectSystem=strict: El sistema de archivos raíz se monta como solo lectura. El proceso no puede escribir en /usr, /etc (a no ser que se marquen explícitamente con
ReadWritePaths=). Esto protege la integridad del sistema incluso si el proceso es comprometido. - ProtectHome: /home, /root y /run/user son inaccesibles. El proceso no puede leer tus claves SSH, tu historial de bash, ni ningún otro dato de usuario.
- ProtectKernelTunables: /sys y /proc/sys se montan como solo lectura. El proceso no puede modificar parámetros del kernel.
- ProtectKernelModules: No se pueden cargar ni descargar módulos del kernel. Un proceso comprometido no puede cargar un módulo malicioso.
- ProtectControlGroups: No se pueden modificar los cgroups. Un proceso no puede escapar de sus limitaciones de recursos.
- MemoryDenyWriteExecute: Impide crear páginas de memoria que sean simultáneamente escribibles y ejecutables. Esto bloquea una técnica común de explotación de vulnerabilidades.
- RestrictRealtime: No se puede usar planificación en tiempo real (SCHED_RR, SCHED_FIFO). Evita que un proceso monopolice la CPU.
- RestrictAddressFamilies: Solo se permiten las familias de direcciones especificadas. Para crustaceo-monitor, que necesita socket UNIX (para systemctl is-active) y posiblemente red local (AF_INET/AF_INET6), esta lista es suficiente. Si solo hicieras comprobaciones locales, podrías limitarte a AF_UNIX.
- SystemCallArchitectures: Solo se permiten llamadas al sistema de la arquitectura nativa (x86_64 en nuestro caso). Esto bloquea cualquier intento de ejecutar código de 32 bits, que a veces se usa para saltarse ciertas restricciones de seguridad.
- SystemCallFilter=@system-service: Whitelist de llamadas al sistema permitidas. El grupo
@system-serviceincluye las syscalls que necesita un servicio normal (read, write, open, close, etc.) y bloquea cientos de syscalls peligrosas (kexec, reboot, bpf, etc.). - LockPersonality: Impide cambiar la personalidad del proceso (un mecanismo de compatibilidad de Linux que permite ejecutar binarios de otras arquitecturas). Otra superficie de ataque eliminada.
- RemoveIPC: Limpia los recursos IPC (semáforos, colas de mensajes, memoria compartida) cuando el proceso termina. No queda basura en el kernel.
- PrivateDevices: El proceso solo ve un /dev mínimo: null, zero, full, random, urandom, tty, ptmx. No tiene acceso a discos, particiones, dispositivos de audio, webcams, etc.
El impacto de todo esto es que crustaceo-monitor se ejecuta en una jaula virtual muy restrictiva. Si alguien comprometiera el binario (por ejemplo, mediante una vulnerabilidad de seguridad), el atacante tendría que saltarse todas estas capas para hacer algo útil. No podría leer /home, no podría escribir en /etc, no podría cargar módulos del kernel, no podría ejecutar binarios con setuid, y no podría usar syscalls peligrosas.
Puedes ver el nivel de seguridad asignado por systemd con:
systemd-analyze security crustaceo-monitor
# → Exposure: 1.2 SAFE 😊
La escala va de 0 (máxima seguridad) a 10 (ninguna protección). Un servicio sin hardening marca típicamente 8-9. Nuestro crustaceo-monitor, con todas las directivas, obtiene una puntuación cercana a 1.
Consejo: No copies y pegues el hardening sin entenderlo. Cada directiva puede romper algo. Si tu daemon necesita leer archivos de /etc,
ProtectSystem=strictlo bloqueará. Si necesita crear sockets,PrivateDevicespuede interferir. Siempre prueba el servicio después de aplicar hardening y mira los logs de systemd para ver si algo falla:journalctl -u crustaceo-monitor -xe.
Proyecto: crustaceo-monitor
Llegó la hora de construir. El proyecto completo:
crustaceo-monitor/
├── Cargo.toml
├── src/
│ ├── main.rs # Punto de entrada, bucle principal, señales
│ ├── config.rs # Carga de configuración TOML
│ └── checks.rs # Comprobaciones: disco, memoria, CPU, servicios
├── crustaceo-monitor.service # Unit file del servicio
├── crustaceo-monitor.timer # Unit file del timer (opcional)
├── crustaceo-monitor.example.toml # Configuración de ejemplo
└── install.sh # Script de instalación
Dependencias (Cargo.toml)
[package]
name = "crustaceo-monitor"
version = "0.1.0"
edition = "2021"
description = "Daemon de monitoreo para systemd: disco, memoria, CPU y servicios"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
anyhow = "1"
chrono = "0.4"
colored = "2"
tokio = { version = "1", features = ["full"] }
sysinfo = "0.32"
Usamos toml con serde para la configuración, sysinfo para las métricas del sistema, chrono para timestamps, colored para logs formateados en terminal, y tokio para el bucle asíncrono y el manejo de señales.
Estructura del código
src/config.rs — Carga la configuración desde un archivo TOML. Si el archivo no existe, usa valores por defecto con un mensaje de aviso. El struct Config tiene tres secciones:
[general]:interval_seconds(cada cuánto comprobar) ylog_alerts_only(si solo mostrar alertas).[thresholds]:disk_percent,memory_percent,cpu_percent— los umbrales de alerta.[services]: lista opcional de servicios systemd a monitorear.
La función Config::load() es simple pero robusta: si el archivo no existe, no crashea, usa defaults. Esto es importante porque el daemon debe poder arrancar aunque la configuración no esté perfecta.
src/checks.rs — Cuatro funciones de comprobación que devuelven CheckResult:
check_disk(): Itera sobre los montajes consysinfo::Disks, calcula el porcentaje de uso.check_memory(): Lee memoria total y usada consysinfo::System.check_cpu(): Lee uso global de CPU.check_service(): Ejecutasystemctl is-active <nombre>y parsea la salida.
Cada resultado tiene un campo CheckStatus que puede ser Ok, Warning o Critical. La función has_critical() permite detectar si hay alertas graves en un ciclo.
src/main.rs — El corazón del daemon:
- Parsea argumentos (
--configpara ruta alternativa). - Carga la configuración.
- Inicializa sysinfo.
- Registra manejadores de señales con
tokio::signal::unix. - Entra en un bucle que cada
interval_secondsejecuta las comprobaciones. - Cada segundo comprueba flags atómicos para SIGUSR1 (check inmediato) o SIGTERM (shutdown).
- Al recibir señal de parada, cierra ordenadamente.
El código completo de cada archivo no lo repetimos aquí porque está en el repositorio del tutorial, pero el flujo es el que ves.
Configuración de ejemplo
# ─────────────────────────────────────────────────────────
# crustaceo-monitor — Configuración de ejemplo
# Copiar a /etc/crustaceo-monitor/config.toml y ajustar
# ─────────────────────────────────────────────────────────
[general]
# Intervalo entre comprobaciones en segundos (por defecto: 60)
interval_seconds = 60
# Si true, solo muestra alertas (omite checks OK)
log_alerts_only = false
[thresholds]
# Umbral de uso de disco (%)
disk_percent = 90.0
# Umbral de uso de memoria (%)
memory_percent = 85.0
# Umbral de uso de CPU (%)
cpu_percent = 90.0
# Servicios systemd a monitorear (opcional)
services = ["sshd", "postgresql", "nginx"]
Unit file del servicio
[Unit]
Description=crustaceo-monitor — Daemon de monitoreo del sistema
Documentation=https://atareao.es/tutorial/rust/
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/crustaceo-monitor --config /etc/crustaceo-monitor/config.toml
User=crustaceo
Group=crustaceo
Restart=on-failure
RestartSec=5
TimeoutStopSec=10
# Variables de entorno
Environment=RUST_BACKTRACE=1
Environment=RUST_LOG=crustaceo_monitor=info
# ── Hardening del servicio ──────────────────────────────
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_SYS_PTRACE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
LockPersonality=true
RemoveIPC=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target
Fíjate en los detalles:
- Type=simple: Systemd no espera ningún mensaje de «ready». En cuanto ejecuta el binario, el servicio está «activo» para systemd.
- User/Group crustaceo: El servicio corre con un usuario del sistema sin permisos de shell. Se crea durante la instalación.
- Restart=on-failure: Si el binario crashea, systemd lo reinicia automáticamente tras 5 segundos.
- TimeoutStopSec=10: Systemd envía SIGTERM y espera 10 segundos. Si no termina, envía SIGKILL.
Unit file del timer (opcional)
Si prefieres no tener el daemon corriendo permanentemente, puedes usar un timer systemd que ejecute el servicio periódicamente:
[Unit]
Description=crustaceo-monitor — Ejecución periódica opcional
Documentation=https://atareao.es/tutorial/rust/
[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=60
[Install]
WantedBy=timers.target
Con este timer, crustaceo-monitor se ejecuta cada hora (con un retardo aleatorio de hasta 60 segundos para evitar picos). Tras la ejecución, termina. No es un daemon persistente, sino una tarea periódica al estilo cron, pero gestionada por systemd.
¿Cuándo usar uno u otro?
- Daemon permanente (el servicio sin timer): Para monitoreo en tiempo real. Te enteras de problemas en segundos.
- Ejecución periódica (servicio + timer): Para tareas que no necesitan inmediatez. Menos recursos consumidos.
Puedes tener ambos instalados y activar/desactivar según necesites.
Script de instalación
El script install.sh automatiza todo el proceso:
#!/usr/bin/env bash
set -euo pipefail
BINARY="crustaceo-monitor"
BIN_SRC="target/release/${BINARY}"
BIN_DST="/usr/local/bin/${BINARY}"
CONFIG_DIR="/etc/${BINARY}"
SERVICE_DST="/etc/systemd/system/${BINARY}.service"
TIMER_DST="/etc/systemd/system/${BINARY}.timer"
# 1. Copiar binario
sudo cp "$BIN_SRC" "$BIN_DST"
sudo chmod 755 "$BIN_DST"
# 2. Crear usuario del servicio
if ! id -u crustaceo &>/dev/null; then
sudo useradd --system --no-create-home --shell /usr/sbin/nologin crustaceo
fi
# 3. Copiar configuración de ejemplo
sudo mkdir -p "$CONFIG_DIR"
sudo cp "${BINARY}.example.toml" "${CONFIG_DIR}/config.toml"
# 4. Copiar unit files
sudo cp "${BINARY}.service" "$SERVICE_DST"
sudo cp "${BINARY}.timer" "$TIMER_DST"
# 5. Recargar y habilitar
sudo systemctl daemon-reload
sudo systemctl enable "${BINARY}.service"
echo "✅ Instalación completada."
echo " sudo systemctl start crustaceo-monitor"
echo " sudo journalctl -u crustaceo-monitor -f"
El script completo está en install.sh dentro del proyecto.
Compilar e instalar
El proceso completo de compilación e instalación:
# 1. Clonar o copiar el proyecto
cd crustaceo-monitor
# 2. Compilar en release (el binario pesa ~1.9 MB)
cargo build --release
# 3. Ejecutar el script de instalación
chmod +x install.sh
sudo ./install.sh
# 4. Editar la configuración (ajustar umbrales, servicios a monitorear)
sudo nano /etc/crustaceo-monitor/config.toml
# 5. Iniciar el servicio
sudo systemctl start crustaceo-monitor
# 6. Verificar que arrancó
sudo systemctl status crustaceo-monitor
Pruebas en desarrollo sin systemd
No necesitas instalar el servicio para probar el daemon durante el desarrollo. Puedes ejecutarlo directamente desde tu terminal:
# Probar la compilación
cargo build
# Ejecutar con un archivo de configuración local (si no existe, usa defaults)
cargo run -- --config ./crustaceo-monitor.example.toml
# El daemon arranca y empieza a mostrar las comprobaciones cada 60s
# Para salir: Ctrl+C (envía SIGINT, el daemon hace graceful shutdown)
# Para probar con un intervalo más corto, crea un config temporal
cat > /tmp/test-config.toml << 'EOF'
[general]
interval_seconds = 5
log_alerts_only = false
[thresholds]
disk_percent = 95.0
memory_percent = 95.0
cpu_percent = 95.0
EOF
cargo run -- --config /tmp/test-config.toml
Durante el desarrollo también puedes probar el envío de señales. Abre dos terminales:
# Terminal 1: ejecuta el daemon
cargo run -- --config /tmp/test-config.toml
# Terminal 2: envía señales
# Forzar comprobación inmediata (simula SIGUSR1)
kill -SIGUSR1 $(pgrep crustaceo-monitor)
# Detener graceful (simula systemctl stop)
kill -SIGTERM $(pgrep crustaceo-monitor)
Esta forma de trabajar acelera el desarrollo: compilas, ejecutas, ves los logs en tiempo real, y matas con Ctrl+C. Sin systemd, sin sudo, sin instalación.
## Verificación
Una vez instalado, verifica que todo funciona:
### Estado del servicio
```bash
sudo systemctl status crustaceo-monitor
La salida debería mostrar algo como:
● crustaceo-monitor.service - crustaceo-monitor — Daemon de monitoreo del sistema
Loaded: loaded (/etc/systemd/system/crustaceo-monitor.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-06-24 10:00:00 CEST; 1min ago
Main PID: 12345 (crustaceo-monito)
Tasks: 4 (limit: 9382)
Memory: 3.2M
CPU: 12ms
CGroup: /system.slice/crustaceo-monitor.service
└─12345 /usr/local/bin/crustaceo-monitor --config /etc/crustaceo-monitor/config.toml
jun 24 10:00:00 servidor crustaceo-monitor[12345]: 🚀 crustaceo-monitor iniciado — PID 12345 — intervalo 60s — config: /etc/crustaceo-monitor/config.toml
jun 24 10:00:00 servidor crustaceo-monitor[12345]: ...comprobaciones iniciales...
jun 24 10:01:00 servidor crustaceo-monitor[12345]: [2026-06-24 10:01:00] OK | disk | Disco OK — pico de uso 45.2%
jun 24 10:01:00 servidor crustaceo-monitor[12345]: [2026-06-24 10:01:00] OK | cpu | CPU OK — 12.3% de uso
jun 24 10:01:00 servidor crustaceo-monitor[12345]: [2026-06-24 10:01:00] ALERT | memory | Memoria al 87.5% — usado: 6.3 GB / 7.2 GB
Logs en tiempo real
# Ver logs en tiempo real del servicio
sudo journalctl -u crustaceo-monitor -f
# Ver logs desde el arranque
sudo journalctl -u crustaceo-monitor --since "1 hour ago"
# Ver solo alertas (críticas)
sudo journalctl -u crustaceo-monitor -p err
Probar señales
# Forzar una comprobación inmediata con SIGUSR1
sudo kill -SIGUSR1 $(systemctl show -p MainPID --value crustaceo-monitor)
# Ver en los logs cómo responde
sudo journalctl -u crustaceo-monitor -n 5
# Deberías ver: "⚡ Recibida SIGUSR1 — forzando comprobación inmediata"
# Apagar el servicio graceful
sudo systemctl stop crustaceo-monitor
# Ver en los logs la parada ordenada
sudo journalctl -u crustaceo-monitor -n 5
# Deberías ver: "⏻ Apagando crustaceo-monitor..."
Probar el timer (opcional)
# Si instalaste el timer
sudo systemctl enable --now crustaceo-monitor.timer
# Ver timers activos
sudo systemctl list-timers --all | grep crustaceo
# Forzar ejecución del timer (sin esperar a la hora)
sudo systemctl start crustaceo-monitor.service
Probar la comprobación de servicios
Si configuraste servicios en el TOML:
# Ejemplo de salida si nginx está caído
sudo journalctl -u crustaceo-monitor | grep "service/"
# [2026-06-24 10:01:00] ALERT | service/nginx | Servicio nginx NO activo (estado: inactive)
Verificar el hardening
# Systemd permite ver qué protecciones están activas
systemd-analyze security crustaceo-monitor
La salida muestra una puntuación de 0 (máxima seguridad) a 10 (ninguna). Nuestro servicio debería obtener una puntuación excelente, cerca de 0-1, indicando que el hardening es muy estricto.
systemd-analyze security crustaceo-monitor
# → Exposure: 1.2 SAFE 😊
Conclusión
En este capítulo hemos visto,
- Escribir un daemon de sistema en Rust que corre como servicio systemd, sin necesidad de daemonización manual (fork, setsid, etc.).
- Manejar señales UNIX (SIGTERM, SIGINT, SIGHUP, SIGUSR1) con
tokio::signalpara graceful shutdown y acciones bajo demanda. - Escribir a journald simplemente usando
println!()— systemd redirige stdout automáticamente. - Configurar un servicio systemd con unit file:
Type=simple,ExecStart,User,Restart, variables de entorno. - Crear un timer systemd como alternativa ligera al daemon permanente, con
OnCalendar=hourly. - Hardening de servicio con
CapabilityBoundingSet,ProtectSystem=strict,PrivateTmp,NoNewPrivileges, y una docena de directivas más de seguridad. - Cargar configuración TOML con serde para umbrales, intervalos y listas de servicios a monitorear.
- Construir un script de instalación que copia el binario, crea el usuario, instala los unit files y habilita el servicio.
Tu código Rust ya no es una herramienta que ejecutas desde la terminal. Es un ciudadano de primera clase en systemd: arranca con el sistema, escribe al log centralizado, respeta las señales del sistema operativo, y se comporta como cualquier otro servicio nativo de Linux.
# Resumen: tu daemon Rust, instalado y funcionando
sudo systemctl status crustaceo-monitor
# ● active (running) — PID 12345 — 3.2M RAM — monitoreando disco, CPU, memoria y servicios
Más información
- Tutorial de systemd en atareao.es — servicios, timers, targets y todo lo que necesitas saber sobre systemd
- Tutorial Scripts en Bash en atareao.es — automatización desde la shell, el punto de partida de muchos linuxeros
- Tutorial Rust en atareao.es — el índice del tutorial completo
- Tutorial Self-Hosted en atareao.es — infraestructura y servicios para tus propios servidores
- systemd.exec(5) — Execution environment configuration — documentación oficial de las directivas de hardening
- systemd.service(5) — Service unit configuration — referencia de los unit files de servicio
- systemd.timer(5) — Timer unit configuration — cómo definir timers en systemd
- Crate sysinfo — información del sistema desde Rust
- Crate toml — parseo de archivos TOML
- Crate signal-hook — manejo de señales en Rust (alternativa a tokio::signal)
- systemd-analyze security — cómo auditar la seguridad de tus servicios