Whenever I want to use a cronjob on Linux, I eventually run into some issues. I’ve spent whole nights debugging why a cronjob didn’t run correctly. By default there doesn’t seem to be any logging support so anything sent to STDOUT or STDERR just disappear. There’s also hardly any sort of indication that the job started except by checking logs.
Instead of writing cronjobs though, you can now use Systemd timers instead. Systemd nicely logs everything so it’s accessible via journalctl
, and allows you to see the next scheduled execution time via systemctl list-timers
.
In this examle I’ve created a quick and dirty python script that just prints to stderr and stdout. If you want to use it, save the following script as executable at /usr/local/bin/script.py
.
#!/usr/bin/python3
import sys
print("Hello from script.py")
print("Writing to stdout")
print("Writing to stderr", file=sys.stderr)
print("Done!")
We first need to create the service systemd unit. As this won’t be a continuously running service, we use the oneshot
service type. Save the following as /etc/systemd/system/script.service
.
[Unit]
Description=A description of what your service does.
[Service]
Type=oneshot
ExecStart=/usr/local/bin/script.py
Note that ExecStart
is not able to pass arguments to the specified program. At least not in a normal way. But for this example it works since it’s just a script with no arguments. If you need to pass arguments you can create a wrapper script.
Next we create the timer itself. It will be connected to the service by having the same filename, but with the different .timer
extension. Save the following as /etc/systemd/system/script.timer
.
[Unit]
Description=Run script every day at noon
[Timer]
OnCalendar=*-*-* 12:00:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar
allows you to set timers to execute on a “calendar” or “realtime” basis. It uses a format of DayOfWeek Year-Month-Day Hour:Minute:Second
and like cron allows you to wildcard each component using *
. Note that the syntax is slightly different
from cron, e.g. to specify every 4 hours you would specify: *-*-* 0/4:00:00
. You can test the format string via systemd-analyze calendar <format string>
. For complete specification see: https://www.freedesktop.org/software/systemd/man/systemd.time.html
It’s also possible to set timers that execute on a “monotonic” basis, meaning they execute at a point relative to e.g. boot. Arch wiki has examples of both realtime and monotonic timers.
Next you need to enable the timer in systemd. You’ll need to run the following scripts.
systemctl daemon-reload # to reload available services
systemctl start script.timer # to start the timer, note that this won't start the timer on next boot.
systemctl enable script.timer # to automatically start the timer on boot (not the [Install section])
To test the timer, you can run the service using systemctl start script
Check logs using journalctl -u script
.
root@mimikyu:/# journalctl -u script
-- Logs begin at Sun 2019-09-08 00:23:42 CEST, end at Sun 2020-02-02 17:21:21 CET. --
Feb 02 17:21:17 mimikyu systemd[1]: Starting A description of what your service does....
Feb 02 17:21:17 mimikyu script.py[14963]: Writing to stderr
Feb 02 17:21:17 mimikyu script.py[14963]: Hello from script.py
Feb 02 17:21:17 mimikyu script.py[14963]: Writing to stdout
Feb 02 17:21:17 mimikyu script.py[14963]: Done!
Feb 02 17:21:17 mimikyu systemd[1]: Started A description of what your service does..
See a table of active timers on your system using systemctl list-timers
. Here’s what it looks like on my laptop.
root@mimikyu:/# systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2020-02-02 18:02:47 CET 38min left Sun 2020-02-02 17:03:00 CET 20min ago anacron.timer anacron.service
Mon 2020-02-03 00:00:00 CET 6h left Mon 2020-01-27 06:33:47 CET 6 days ago fstrim.timer fstrim.service
Mon 2020-02-03 01:37:32 CET 8h left Sat 2020-02-01 12:59:50 CET 1 day 4h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2020-02-03 04:47:10 CET 11h left Sun 2020-02-02 11:28:40 CET 5h 55min ago apt-daily.timer apt-daily.service
Mon 2020-02-03 06:33:12 CET 13h left Sun 2020-02-02 11:28:40 CET 5h 55min ago apt-daily-upgrade.timer apt-daily-upgrade.service
Mon 2020-02-03 06:35:38 CET 13h left Sun 2020-02-02 16:58:23 CET 25min ago motd-news.timer motd-news.service
n/a n/a Thu 2020-01-30 08:27:59 CET 3 days ago ureadahead-stop.timer ureadahead-stop.service
8 timers listed.
Pass --all to see loaded but inactive timers, too.
From what I understand, this can also be done on a user-level basis. So the the service will execute with that user’s profile. It works similarily but you put the systemd unit files in different directories. E.g. ~/.config/systemd/user/
. The arch wiki has a detailed page about it.
https://wiki.archlinux.org/index.php/Systemd/Timers https://www.freedesktop.org/software/systemd/man/systemd.service.html https://wiki.archlinux.org/index.php/Systemd/User