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-timersshows 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 calendarreports no elapse — the expression is invalid. Check the syntax inman 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:00is 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.