How to write a simple systemd timer

The modern approach to run a command periodically in Linux

Author's image
Tamás Sallai
4 mins

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 once
  • enable => 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.
May 3, 2022
In this article