SYSTEMD · 2026-11-03

systemd: reiniciar un servicio a diario, sin cron

Reinicia un servicio systemd a diario con un timer, no una línea cron: OnCalendar=daily, RandomizedDelaySec, Persistent. Bench systemd 262 — recibos dentro.

HUMANpor @mrkeepalive1h7 min de lectura

El timer es la vía integrada para reiniciar un servicio systemd en un horario. OnCalendar=daily elige la medianoche, RandomizedDelaySec reparte la carga después de las 00:00, Persistent=true recupera lo perdido tras una caída. Cron puede ejecutar el mismo reinicio — pero a ciegas, exactamente a medianoche, junto a todos los demás jobs de la máquina y sin registro de cuándo disparó por última vez. En el bench de esta noche el timer disparó tres veces en noventa y cinco segundos y el journal nombró cada una a la décima de segundo.

¿Cómo es la receta del reinicio diario?

Dos archivos. Uno sabe un solo truco — reiniciar la app. El otro decide cuándo. Ninguno toca la unidad de la propia app.

# /etc/systemd/system/app-restart.service
[Unit]
Description=scheduled restart of app

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl try-restart app.service

# /etc/systemd/system/app-restart.timer
[Unit]
Description=restart app once a day

[Timer]
OnCalendar=daily
RandomizedDelaySec=45m
Persistent=true

[Install]
WantedBy=timers.target

verifica: sudo systemctl daemon-reload && sudo systemctl enable --now app-restart.timer && systemctl list-timers app-restart.timer — una columna NEXT poblada significa que el timer ya es dueño del horario.

try-restart, no restart: si la app pasa a estar caída, un restart simple la arrancaría en silencio — quizá resucitando lo que apagaste a propósito. try-restart solo toca lo que ya está corriendo. Los servicios que nunca deben quedar caídos cambian a restart y duermen tranquilos.

OnCalendar=dailyla rueda del calendario · 00:00 RandomizedDelaySechasta 45 min, sorteado en cada disparo Persistent=trueel sello · los días perdidos se disparan al arrancar oneshot · try-restartsolo si la app está corriendo la app vuelve, frescamisma unidad, PID nuevo
Cinco piezas móviles, un reinicio diario. El sello es el único estado en disco — un archivo de cero bytes.

¿Por qué no usar simplemente cron?

Porque la muleta de cron funciona una vez y luego se apoya en ti. @daily systemctl restart app es una línea en el crontab de root — y las preguntas empiezan. ¿Cuándo disparó por última vez? ¿Disparó siquiera mientras la máquina estaba apagada? ¿Por qué todos los jobs de la máquina sellaron 00:00:00 juntos? La página man de cron define @daily como medianoche en punto; cada servicio programado así pasa el mismo torno en el mismo segundo, y el primero en pasar es el disco.

La contabilidad honesta: cron reinicia la app, y eso es todo lo que hace. Sin columna de list-timers, sin sello en disco, sin recuperación tras una caída, sin unidad en el journal que leer — si es que el demonio de cron está siquiera instalado. La imagen base de nuestra VM de bench no trae ninguno. En una máquina Arch moderna los timers ya están ahí, bajo el PID 1, esperando.

la preguntacron @dailyel timer
¿Cuándo disparó por última vez?grep a los logs, quizálist-timers, columna LAST
¿Máquina apagada a medianoche?saltado, en silencioPersistent dispara al arrancar
¿Cuándo exactamente se ejecuta?00:00:00 en punto00:00 más el retardo sorteado
¿Un demonio extra que mantener?cron o crondninguno — el PID 1 ya lo hace
¿Dónde están los logs?por correo, si acasojournalctl -u, estructurados

El caso general — cuándo un timer le gana a una línea de cron y por qué — vive en nuestra pieza anterior, Cron contra los timers de systemd: cron ejecuta líneas, los timers ejecutan unidades. Esta es solo el caso especial del reinicio nocturno.

¿Cómo detengo la estampida de medianoche?

Una línea: RandomizedDelaySec=45m. Tras cada vencimiento del calendario el timer sortea un retardo nuevo, así que una docena de reinicios nocturnos cae en cualquier punto de la ventana en vez de en un solo instante. El bench corrió una versión en escala de minutos de la receta — calendario al :30, retardo topado en 20 segundos — y cada sorteo cayó en otro lugar:

recibos del bench, sin editar — systemd 262 en Arch
Nov 03 05:13:48.974739 bench-arch systemd[1]: Starting scheduled restart of app...
RandomizedDelayUSec=20s
LastTriggerUSec=Tue 2026-11-03 05:14:40 UTC
NextElapseUSecRealtime=Tue 2026-11-03 05:15:43 UTC

Lee la aritmética en las líneas: calendario 30, tope 20 — los sorteos salieron a los 18.97, 10.42 y 13 segundos. Tres disparos, tres segundos distintos, todos dentro del tope. Cron no sabe sortear; su man programa al segundo fijo, por siempre. Vuelve a subir a 45m y tus reinicios nocturnos se reparten por la ventana, y el disco nunca se encuentra con una multitud.

¿Y si la máquina estaba apagada a la hora del reinicio?

Persistent=true es la cláusula de recuperación. Tras cada disparo el timer toca un sello de cero bytes — /var/lib/systemd/timers/stamp-app-restart.timer — y el systemd del arranque lo compara con el calendario. Máquina apagada toda la noche, arranque a las 09:00: el reinicio perdido se ejecuta de inmediato, una vez. Sin la línea, el disparo perdido se salta en silencio y la app espera un día entero su reset. El sello es una promesa de cero bytes: te debo un reinicio después del próximo arranque.

el directorio de sellos en el bench, sin editar
-rw-r--r-- 1 root root 0 Nov  3 05:13 stamp-app-restart.timer
-rw-r--r-- 1 root root 0 Oct 24 10:42 stamp-archlinux-keyring-wkd-sync.timer

La segunda línea no es nuestra: la imagen base trae ese timer ya activado y sellado. El patrón es el hábito del planificador por defecto — las actualizaciones del keyring de Arch se recuperan tras una caída igual que lo hará tu app.

¿Acaso Restart=always es lo mismo?

No, y confundirlos es como se inventan los apagones de medianoche. Restart=always se guía por el fallo: el proceso principal sale, systemd lo levanta de nuevo tras RestartSec. Mientras la app corre sana, nunca dispara — no tiene nada a qué reaccionar. No puede hacer mantenimiento de calendario, porque la salud no es una fecha. El timer se guía por el calendario: llegan las 00:00, el reinicio se ejecuta, sano o no. Conserva ambos si necesitas ambos — Restart=always para los crashes, el timer para el reset nocturno. Vigilan relojes distintos y nunca pelean.

¿Cómo compruebo que realmente disparó?

systemctl list-timers app-restart.timer --no-pager
journalctl -u app-restart.service --since today --no-pager

verifica: después de medianoche, NEXT se repuebla para mañana y LAST queda dentro de tu ventana de retardo. La línea del journal que importa dice Starting scheduled restart of app... — seguida del stop y del arranque de la propia app:

el journal de la propia app en torno a un disparo del bench, sin editar
Nov 03 05:13:48.990989 bench-arch systemd[1]: Stopping the app that gets restarted nightly...
Nov 03 05:13:48.993916 bench-arch systemd[1]: Started the app that gets restarted nightly.

Un reinicio nocturno es una confesión: algo tiene fugas, y nadie ha encontrado aún el agujero.

Cambia tu servicio esta noche con la checklist y deja que el timer cargue la confesión hasta que alguien arregle la fuga de verdad — luego borra ambas unidades y deja que la máquina duerma pasada la medianoche. Esa es la condición de salida, y el archivo de sello no te va a extrañar.

El método del bench, para leer los recibos en crudo

VM Arch desechable (imagen cloud de quickemu, sin cambios en el host): un app.service corriendo sleep infinity, la receta del timer escalada a OnCalendar=*-*-* *:*:30 con RandomizedDelaySec=20 para que un disparo caiga dentro de un minuto de espera, y luego 95 segundos mirando el journal. Tres disparos, el archivo de sello, las columnas de list-timers y la versión de systemd están en un solo log de recibos, impreso sin editar arriba. El tercer disparo del bench cayó sobre un reloj que la VM seguía ajustando al arrancar; su aritmética es la única línea que esta pieza no defiende, y el log la conserva.