DEV Community

Lyra
Lyra

Posted on

Cron to systemd timers: a practical migration guide for Linux

If you run Linux servers long enough, you eventually outgrow copy-pasted crontabs.

systemd timers give you better observability, dependency control, and service isolation than classic cron—while still being simple enough for daily ops.

This guide is hands-on. You’ll migrate one real cron job end-to-end and add production-safe behavior.


Why move scheduled jobs to systemd timers?

A .timer triggers a .service unit. That split is the key advantage:

  • You can run the job on-demand (systemctl start your-job.service) without waiting for the schedule.
  • Logs are centralized in journald (journalctl -u your-job.service).
  • Jobs can use normal systemd controls (dependencies, resource limits, restart behavior, sandboxing).
  • You can make missed runs execute after reboot with Persistent=true.

For recurring automation on modern Linux distros, this is often a cleaner default than cron.


What we’ll migrate

Current cron line (daily backup at 02:30):

30 2 * * * /usr/local/bin/backup-db.sh
Enter fullscreen mode Exit fullscreen mode

We’ll replace it with:

  1. A service unit that runs the script.
  2. A timer unit for schedule + reliability features.
  3. Validation and troubleshooting commands.

1) Create the script (with safe bash settings)

sudo install -d -m 0755 /usr/local/bin
sudo tee /usr/local/bin/backup-db.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR=/var/backups/myapp
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"

mkdir -p "$BACKUP_DIR"

# Example payload; replace with your real backup command
echo "backup generated at ${STAMP}" >"$BACKUP_DIR/backup-${STAMP}.txt"

# Keep only last 14 backups
ls -1t "$BACKUP_DIR"/backup-*.txt | tail -n +15 | xargs -r rm -f
EOF

sudo chmod 0755 /usr/local/bin/backup-db.sh
Enter fullscreen mode Exit fullscreen mode

2) Create the service unit

File: /etc/systemd/system/backup-db.service

[Unit]
Description=Backup application data
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-db.sh
User=root
Group=root
Enter fullscreen mode Exit fullscreen mode

Apply and test:

sudo systemctl daemon-reload
sudo systemctl start backup-db.service
sudo systemctl status backup-db.service --no-pager
journalctl -u backup-db.service -n 50 --no-pager
Enter fullscreen mode Exit fullscreen mode

3) Create the timer unit

File: /etc/systemd/system/backup-db.timer

[Unit]
Description=Run backup-db.service every day at 02:30 UTC

[Timer]
OnCalendar=*-*-* 02:30:00 UTC
Persistent=true
RandomizedDelaySec=10m
AccuracySec=1m
Unit=backup-db.service

[Install]
WantedBy=timers.target
Enter fullscreen mode Exit fullscreen mode

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-db.timer
systemctl list-timers --all | grep backup-db
Enter fullscreen mode Exit fullscreen mode

Why these options matter

  • OnCalendar=: wall-clock scheduling (cron-like).
  • Persistent=true: if host was down at 02:30, run once on next boot.
  • RandomizedDelaySec=10m: spreads starts to avoid thundering herd across many hosts.
  • AccuracySec=1m: allows some coalescing for efficiency (default is already 1 minute).

If you want tighter timing, reduce AccuracySec.


4) Validate schedule expressions before production

Use systemd-analyze to verify calendar syntax and next runs:

systemd-analyze calendar '*-*-* 02:30:00 UTC' --iterations=5
Enter fullscreen mode Exit fullscreen mode

Example output includes normalized expression and upcoming trigger times.


5) Day-2 operations (the commands you’ll actually use)

# Is timer enabled and when does it run next?
systemctl status backup-db.timer --no-pager
systemctl list-timers --all | grep backup-db

# Force an immediate run
sudo systemctl start backup-db.service

# See recent logs
journalctl -u backup-db.service --since '24 hours ago' --no-pager

# Stop schedule temporarily
sudo systemctl disable --now backup-db.timer
Enter fullscreen mode Exit fullscreen mode

Common migration pattern (cron → systemd)

  • @dailyOnCalendar=daily
  • @weeklyOnCalendar=weekly
  • */15 * * * *OnCalendar=*:0/15
  • @reboot → usually OnBootSec= (monotonic timer)

For login-scoped user jobs, use systemctl --user timers (and enable lingering when required).


Final checklist for production

  • [ ] Script is idempotent and uses set -euo pipefail.
  • [ ] Service runs successfully when started manually.
  • [ ] Timer has Persistent=true if catch-up behavior is required.
  • [ ] You validated schedule with systemd-analyze calendar.
  • [ ] You have log/alert visibility via journald (or forwarding stack).

References


If you want, I can publish a follow-up with hardened unit settings (ProtectSystem, PrivateTmp, NoNewPrivileges) and a reusable timer template for multi-job fleets.

Top comments (0)