Scheduling Tasks with Systemd Timers
John L. Carveth
I have been using systemd
to manage services on my many Linux boxes for a while now. Compared to writing initd scripts, systemd service files are dead simple.
Recently, I needed to setup a backup solution for a new linux box being setup at work. This server was running Ubuntu Server 22.04, which comes with systemd 249 (249.11-0ubuntu3.4)
out of the box.
Our ideal backup solution would backup only the necessary files at a regular interval. The backup would be stored to an external disk as well as sent to a remote server off-site. Thankfully, we can configure our systemd service to depend upon a specific mount point, meaning the script will only run if the proper disk is connected to the server.
Two unit files are needed to accomplish our goal, in additon to a shell script:
backup.service
- This is the service that will run our backup script.backup.timer
- The timer that activates and controls the service unit.backup.sh
- The script that actually handles the backup.
I went ahead and stored all three files within their own directory under /opt/
.
Let's take a look at backup.timer
:
[Unit]
Description=Timer unit for backup.service
Requires=backup.service
[Timer]
Unit=backup.service
OnCalendar=daily
[Install]
WantedBy=multi-user.target
Quite a simple file, with only five important lines. Requires
specifies that backup.timer
relies on backup.service
in order to run correctly. Within the [Timer]
section, Unit
specifies the service to run on schedule, and OnCalendar
specifies the frequency at which the service will run. Our timer will execute backup.service
daily.
One thing I learned while setting up this timer unit is the difference between timespan and timestamps. OnCalendar
expects a timestamp value, such as daily
or another value such as:
- minutely, hourly, daily
- 2022-07-26 12:00:00
- Mon,Fri *-01/2-01,03 *:30:45
However, what if you want the script to run every 30 minutes? In that case, we'd use a timespan. This page from the Arch Linux manual covers the timespan format quite well.
Examining backup.service
shows a similarly simple unit file:
[Unit]
Description=SystemBackup
Wants=backup.timer
After=media-jlcarveth-JLCT7.mount
[Service]
Type=oneshot
ExecStart=/opt/backup/backup.sh
[Install]
WantedBy=multi-user.target
This service will only run once another service media-jlcarveth-JLCT7.mount
has finished running, meaning the disk has been mounted. This service runs once, as specified by OneShot
, and simply executes a shell script.
Finally, let's take a look at our backup script, backup.sh
:
#!/bin/sh
# File containing paths to backup
TARGETS="/opt/backup/to_backup"
# Create a new dated directory for the backup
BACKUP_DEST="/opt/backup/current"
mkdir -p $BACKUP_DEST
while read line;
do
include_args="${include_args} ${line}"
done <$TARGETS;
# Execute the rsync command
# -a : Archive mode, keeps file properties intact
# -L : Copy links, transform symlinks into referent file / dir
# -v : Verbose mode
# -R : Relative mode, use relative path names
/usr/bin/rsync -aLvR ${include_args} $BACKUP_DEST
printf "Backup Completed $(date +%FT%r)\n" >> /opt/backup/history.log
Again, dead simple. This script reads each line from a file to_backup
which contains absolute paths to each directory/file I wish to backup. These paths are appended to an argument that is then passed to rsync.
The result is a folder current/
containing a perfect mirror of all those directories previously specified. The next time rsync runs, it will also execute quicker as it doesn't need to copy each file and can simply compare them.