Skip to main content
RunBook Academy

← All labs in Linux

Lab · intermediate · ~45 min

Lab: Replace cron jobs with systemd timers

B · Nested virtualisationC · Simulation

Objectives

  • Inspect an existing crontab and identify candidate jobs for migration
  • Write a service unit for each candidate
  • Write a timer unit with the appropriate OnCalendar or OnUnitActiveSec expression
  • Validate the timer expression with systemd-analyze calendar

Prerequisites

This lab migrates existing cron entries to systemd timers. The result is the same schedule, with better observability (journal), better dependency handling (After=, Requires=), and the ability to add resource limits and hardening directives.

Objective

By the end of this lab, you can audit a crontab, write the corresponding systemd units, and verify that the schedule matches the original cron expression.

Architecture

flowchart LR
  CRON[Existing crontab]
  CAT[Categorise jobs]
  SVC[Service unit]
  TMR[Timer unit]
  ACT[systemd-analyze calendar]
  VAL[Validate]

  CRON --> CAT --> SVC --> TMR --> ACT --> VAL

Requirements

  • A Linux host (Ubuntu 24.04 LTS or Debian 12 preferred).
  • Root or sudo access.
  • An existing crontab with at least 3 entries (root’s or a service account’s).

Scenario

Your host has the following crontab:

# /etc/crontab or root\'s crontab
0 2 * * *   /usr/local/bin/backup.sh
*/15 * * * * /usr/local/bin/health-check.sh
@reboot    /usr/local/bin/startup-checks.sh
0 0 * * 0  /usr/local/bin/weekly-report.sh

You need to migrate each entry to a systemd timer while preserving the original schedule and improving the integration with the rest of the system.

Tasks

Task 1: Inventory the crontab

crontab -l
ls /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly 2>/dev/null

For each entry, note:

  • The schedule (cron expression).
  • The command.
  • The user (root’s crontab, a service account, /etc/cron.d/…).
  • The expected resource use (CPU, memory, I/O).

Task 2: Categorise

For each entry, decide:

  • Backup-style: scheduled at off-hours; should be a timer with OnCalendar.
  • Health check: frequent (every 15 minutes); should be a timer with OnCalendar and RandomizedDelaySec.
  • Startup check: runs once at boot; should be a timer with OnBootSec=.
  • Weekly report: weekly at midnight; should be a timer with OnCalendar.

Task 3: Write service units

Create a service unit for each:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/health-check.service
[Unit]
Description=Periodic health check

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/health-check.sh
# /etc/systemd/system/startup-checks.service
[Unit]
Description=One-shot startup checks

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/startup-checks.sh
# /etc/systemd/system/weekly-report.service
[Unit]
Description=Weekly operations report

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/weekly-report.sh

If the scripts do not exist on your lab host, create stubs:

sudo tee /usr/local/bin/backup.sh >/dev/null <<'EOF'
#!/bin/bash
echo "backup started at $(date -Is)"
sleep 1
echo "backup finished"
EOF
sudo chmod +x /usr/local/bin/backup.sh

Repeat for each script.

Task 4: Write timer units

Translate the cron expressions to systemd syntax. Validate each with systemd-analyze calendar:

systemd-analyze calendar "*-*-* 02:00:00"
systemd-analyze calendar "*-*-* *:00/15:00"
systemd-analyze calendar "Sun *-*-* 00:00:00"
# /etc/systemd/system/backup.timer
[Unit]
Description=Nightly backup timer

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=15min

[Install]
WantedBy=timers.target
# /etc/systemd/system/health-check.timer
[Unit]
Description=Periodic health check timer

[Timer]
OnCalendar=*-*-* *:00/15:00
RandomizedDelaySec=30s

[Install]
WantedBy=timers.target
# /etc/systemd/system/startup-checks.timer
[Unit]
Description=Startup checks timer

[Timer]
OnBootSec=2min
OnUnitInactiveSec=1h

[Install]
WantedBy=timers.target
# /etc/systemd/system/weekly-report.timer
[Unit]
Description=Weekly report timer

[Timer]
OnCalendar=Sun *-*-* 00:00:00
Persistent=true
RandomizedDelaySec=30min

[Install]
WantedBy=timers.target

Task 5: Activate and verify

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer health-check.timer startup-checks.timer weekly-report.timer
systemctl list-timers backup.timer health-check.timer startup-checks.timer weekly-report.timer

The list-timers output should show all four timers with their next elapse times.

Task 6: Test activation

sudo systemctl start backup.service
journalctl -u backup.service -n 20 --no-pager

The service should run; the journal should capture its output. Confirm the journal includes the expected log lines.

Task 7: Disable the crontab

Only after confirming all four timers fire correctly:

# `sudo crontab -l > /root/...` does NOT work: the shell opens the output
# file before sudo runs, so the write happens as your unprivileged user and
# fails with "Permission denied" on /root. Pipe through `sudo tee` instead.
BAK=/root/crontab.bak.$(date +%F-%H%M%S)
sudo crontab -l | sudo tee "$BAK" >/dev/null

# Prove the backup exists and is not empty BEFORE removing anything
sudo test -s "$BAK" && echo "backup ok: $BAK"

sudo crontab -r

The crontab is backed up and removed. The systemd timers now own the schedule.

Validation

The lab is complete when:

  • All four crontab entries have corresponding systemd units.
  • systemctl list-timers shows all four timers with the expected next-elapse times.
  • A manual systemctl start <service> runs the service and the journal captures its output.
  • The original crontab has been backed up and removed.

Expected outcome

Four systemd units that fire at the same schedule as the original cron entries, with the operational benefits of journal logging, dependency ordering, and resource controls (which could be added to each service unit as needed).

Troubleshooting

  • systemd-analyze calendar reports no elapse — the expression is invalid. Check the syntax in man 7 systemd.time.
  • Timer fires but service does not run — check the journal for the service: journalctl -u <service>. The ExecStart may have failed.
  • The schedule does not match the original cron — compare carefully. 0 2 * * * is “02:00 every day”; *-*-* 02:00:00 is the systemd equivalent.

Cleanup

sudo systemctl disable --now backup.timer health-check.timer startup-checks.timer weekly-report.timer
sudo systemctl stop backup.service health-check.service startup-checks.service weekly-report.service
sudo rm /etc/systemd/system/backup.{service,timer}
sudo rm /etc/systemd/system/health-check.{service,timer}
sudo rm /etc/systemd/system/startup-checks.{service,timer}
sudo rm /etc/systemd/system/weekly-report.{service,timer}
sudo systemctl daemon-reload

Restore the original crontab if desired:

sudo crontab /root/crontab.bak.YYYYMMDD

What you learned

You can now audit an existing crontab, design the migration to systemd timers, and validate the schedule matches. The discipline is to verify each timer fires before disabling the corresponding crontab entry.

Deliverables

  • · A list of crontab entries migrated to systemd units
  • · Service unit files at /etc/systemd/system/
  • · Timer unit files at /etc/systemd/system/
  • · Output of systemctl list-timers after migration

Verification status

Last reviewed
2026-08-09
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.