Rocky 10: replacing cron with systemd timers
Test scope
Section titled “Test scope”In a Rocky Linux 10.2 privileged container (systemd 257, running as PID 1 inside the container): wrote lab-tick.service + lab-tick.timer, ran systemctl enable --now, list-timers, a manual start, then disable and cleanup. Log: plans/content-expansion/evidence/logs/rocky-systemd-timers.log.
The container shares the host kernel (5.14); systemd 257 is genuine EL10 userspace. Anything about kernel scheduling belongs on a real host.
Why a timer instead of cron
Section titled “Why a timer instead of cron”- Output goes to the journal (
journalctl -u), no manual redirection; - You can declare dependencies (
After=,Requires=) that match boot ordering; Persistent=trueruns a missed job once after boot;- Accuracy and jitter are controllable (
AccuracySec,RandomizedDelaySec).
cron stays smaller and more portable; the choice depends on whether you need those properties.
The pair of files
Section titled “The pair of files”[Unit]Description=lab tick[Service]Type=oneshotExecStart=/usr/local/bin/lab-tick.sh[Unit]Description=lab tick timer[Timer]OnCalendar=*:0/1Persistent=true[Install]WantedBy=timers.targetThe timer and service must share a name (lab-tick.timer triggers lab-tick.service). Type=oneshot fits scripts that run and exit.
Enable it and read the next run
Section titled “Enable it and read the next run”sudo systemctl daemon-reloadsudo systemctl enable --now lab-tick.timersystemctl list-timers --all | grep -E "lab-tick|NEXT"NEXT LEFT LAST PASSED UNIT ACTIVATESTue 2026-10-20 11:48:00 UTC 2s - - lab-tick.timer lab-tick.serviceNEXT is the next trigger and ACTIVATES points at the paired service. Those two columns are how you confirm a timer works, no guessing required.
Trigger it manually
Section titled “Trigger it manually”sudo systemctl start lab-tick.servicetail -2 /var/log/lab-tick.log2026-10-20T11:47:58ZStarting the service does not change the timer schedule; it validates the script itself.
Clean up
Section titled “Clean up”sudo systemctl disable --now lab-tick.timersudo rm -f /etc/systemd/system/lab-tick.{service,timer} /usr/local/bin/lab-tick.shsudo systemctl daemon-reloaddisable --now stops the timer and removes the symlink under timers.target.wants (the observed output was Removed '.../lab-tick.timer').
Common traps
Section titled “Common traps”- Enabling only the service: the timer never starts and never appears in
list-timers; - Mismatched names:
foo.timeronly triggersfoo.service; - Wrong OnCalendar:
*:0/1means every minute, not every hour; check withsystemd-analyze calendar.
Practice: a timer that runs every minute
Section titled “Practice: a timer that runs every minute”sudo tee /etc/systemd/system/lab-tick.service >/dev/null <<'EOF'[Unit]Description=lab tick[Service]Type=oneshotExecStart=/bin/dateEOFsudo tee /etc/systemd/system/lab-tick.timer >/dev/null <<'EOF'[Unit]Description=lab tick timer[Timer]OnCalendar=*:0/1[Install]WantedBy=timers.targetEOFsudo systemctl daemon-reloadsudo systemctl enable --now lab-tick.timersystemctl list-timers lab-tick.timersystemd-analyze calendar '*:0/1'Normalized form: *-*-* *:00/1:00 Next elapse: Tue 2026-10-20 11:50:00 UTC From now: 53s leftChecks:
NEXTinlist-timersis within 60 secondssystemd-analyze calendar '*:0/1'printsNext elapse:andIterationsystemctl status lab-tick.timershowsactive (waiting)- After
disable --now,list-timersno longer lists it
常见问题
The timer fired but the script did not run — where do I look?
Check systemctl status <name>.timer for active, then journalctl -u <name>.service for the invocation; if the names do not match, the timer never triggers that service.
How many times does Persistent=true catch up?
Once — missed windows are merged into a single run, not replayed per occurrence, which is usually what backup jobs want.
Is the syntax the same on EL9?
Timer unit syntax is identical; EL10 ships systemd 257 versus roughly 252 on EL9, so check man systemd.timer for newer OnCalendar extensions.
Next steps
Section titled “Next steps”- Log rotation: logrotate in practice
- Reboot policy: Reboot policy
- Firewall rules: firewalld rich rules