How to write a simple systemd timer
The modern approach to run a command periodically in Linux
Scheduling tasks in Linux
I have a Linux system and I wanted a monitoring script to run periodically. Easy, right? Just use Cron. Well, after a bit of searching, I realized that my understranding of Linux tools is not one but two generations out of date. So instead of Cron, I started learning about systemd and how to run timers using it.
This article is a summary of what I learned in the process and what to do if you only need to run a command periodically with systemd.
Systemd services
Services live in the /etc/systemd/system
directory, so adding a new one and a timer is a matter of creating files here. There are also services in the
/lib/systemd/system
directory, but those are installed from packages. For any local additions, use /etc
.
Running a command on a schedule is broken into two parts: the service (what to run) and the timer (when to run it). This makes it easier to test the command in isolation as you can run it without the timer.
Service
So I created a file at /etc/systemd/system/syncthing-monitoring.service
with the configuration I want to run periodically:
[Unit]
Description=Syncthing monitoring
[Service]
User=...
Group=...
Environment="TOKEN=..."
Environment="CHAT_ID=..."
Type=oneshot
ExecStart=/usr/bin/node /home/ubuntu/dotfiles/syncthing-monitoring/monitoring.mjs
This service has a lot more configuration than the bare minimum, so let's see what each part does!
The User
and the Group
defines which user should run the command. Without this, the root
will run it.
The Environment
defines environment variables. Since my script relies on the TOKEN
and the CHAT_ID
variables present, this is the best place
to define them.
The Type=oneshot
defines that the command should be run once and it's not a service that systemd should keep running (those are usually called "daemons").
Then the ExecStart
is the command to run. It accepts only full paths, so there is no working directory here. The command above runs a NodeJs script.
Timer
The timer goes to /etc/systemd/system/syncthing-monitoring.timer
:
[Unit]
Description=Syncthing monitoring
[Timer]
OnBootSec=5m
OnUnitActiveSec=1h
[Install]
WantedBy=timers.target
The Description
shows in the output of many commands, so it's a best practice to provide something here.
Then the OnBootSec
and the OnUnitActiveSec
defines that the service will be run 5 minutes after boot and every hour after that.
The WantedBy
defines a dependency in systemd. Here, the timers.target
will depend on this timer, so when the system activates timers during boot it
will also activate this one.
The timer finds the service based on the name (syncthing-monitoring.timer
=> syncthing-monitoring.service
) but that's also configurable.
Activating and running timers
Now that both the service and the timer are configured, the only task left is to run them. For this, systemd provides two mechanisms:
start
=> run the timer onceenable
=> run the timer on boot
Usually, you want the latter, as that makes sure that the timer will survive restarts.
The commands to enable/start the timer:
sudo systemctl enable syncthing-monitoring.timer
sudo systemctl start syncthing-monitoring.timer
To monitor, systemd provides a status
command:
$ sudo systemctl status syncthing-monitoring.timer
● syncthing-monitoring.timer - Syncthing monitoring
Loaded: loaded (/etc/systemd/system/syncthing-monitoring.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Fri 2022-04-15 09:27:21 CEST; 1h 33min ago
Trigger: Fri 2022-04-15 11:32:16 CEST; 31min left
Triggers: ● syncthing-monitoring.service
Apr 15 09:27:21 ip-172-31-3-165 systemd[1]: Started Syncthing monitoring.
And since the service is separated from the timer, you can query the status of the service too:
$ sudo systemctl status syncthing-monitoring.service
● syncthing-monitoring.service - Syncthing monitoring
Loaded: loaded (/etc/systemd/system/syncthing-monitoring.service; static; vendor preset: enabled)
Active: inactive (dead) since Fri 2022-04-15 10:32:17 CEST; 29min ago
TriggeredBy: ● syncthing-monitoring.timer
Main PID: 13050 (code=exited, status=0/SUCCESS)
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: paused: false,
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: lastSeen: 2022-04-15T08:31:51.000Z
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: }
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: ],
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: systemErrors: null,
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: pendingFolders: {},
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: pendingDevices: {}
Apr 15 10:32:17 ip-172-31-3-165 node[13050]: }
Apr 15 10:32:17 ip-172-31-3-165 systemd[1]: syncthing-monitoring.service: Succeeded.
Apr 15 10:32:17 ip-172-31-3-165 systemd[1]: Finished Syncthing monitoring.