Ubuntu: Running a bash script periodically with a user-level Systemd timer

If you have a Bash script that needs to run periodically, you can run it using a crontab entry.  But you can also have it invoked by Systemd using systemd.timer.

Furthermore, you can run Systemd services as  user-level services instead of the typical system-level service for even further isolation.

Running via Systemd provides more powerful constructs for invocation, configuration, monitoring, and logging. In this article, I will show how to periodically run a simple Bash script using a user-level Systemd timer.

Prerequisites

Install the package prerequisites and download the github project code.

# packages
sudo apt install curl git -y

# pull project
git clone https://github.com/fabianlee/systemd-periodic-bash.git
cd systemd-periodic-bash

Running user-level Systemd service

Now use the supplied script to create the user-level Systemd service and timer, and run the Bash script every 60 seconds. We will go into the details of how this works in the upcoming sections.

cd systemd-userlevel

# create user specific systemd files, start
./create-simple-userservice.sh

# configure environment variable
# set 'env_arg' to any value, I set mine to 'my foo user'
vi ~/default/simple

If you tail the logs, you should see output similar to below every 60 seconds.

$ tail -f ~/log/simple/simple.log

========================
Mon 03 Jan 2022 08:10:38 PM EST
========================
env_arg environment variable: my foo fabian
loop counter 1
loop counter 2
loop counter

Overview

Because we are running this Bash script via Systemd, we get a lot of production-level features over using a simple cron. Basically, we get all the power of Systemd.

  • Invocation – run once at boot, periodically, or after the network is initialized
  • Isolation – as a specific user or group
  • Configuration – pull in properties from ‘/etc/default’ or other locations
  • Monitoring – process monitored for success/error, watchdog process
  • Logging – to syslog or file

Systemd service unit file

Our simple.service file is shown below.  The “$userdir” and “$scriptdir” values are placeholders, replaced by create-simple-userservice.sh.

[Unit]
Description=Simple service user level
ConditionPathExists=$scriptdir/simple-bash.sh
After=network.target
 
[Service]
Type=forking
#User=
#Group=

# define 'env_arg' env var, secured by root ownership and mode 600
EnvironmentFile=-$userdir/default/simple

WorkingDirectory=$scriptdir
ExecStart=$scriptdir/simple-bash.sh 3

# make sure log directory exists and owned by syslog
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p $userdir/log/simple
ExecStartPre=/bin/chmod -R 755 $userdir/log
StandardOutput=append:$userdir/log/simple/simple.log
StandardError=append:$userdir/log/simple/simple.err

This has the effect of:

  • Starting the service after the network is initialized (After=network.target)
  • Running the service as the current user (#User commented out)
  • Loading environment keys from an optional default file (EnvironmentFile)
  • Invoking the Bash script with a command line argument (ExecStart)
  • Hooks that run before the invocation (ExecStartPre)
  • Appending output of script to file (StandardOutput)

service and timer files

The .service and .timer files used by Systemd are kept in “~/.config/systemd/user” for user-level services.

These are enabled and started like this.

service=simple

# enable and start service and timer
systemctl --user enable $service.service $service.timer
systemctl --user start $service.service
systemctl --user start $service.timer

 

 

 

REFERENCES

debian, systemd.service docs

lostsaloon.com, running cron as specific user

silentlad.com, explanation of systemd timers OnCalendar

man7.org, man page for systemd.timer

 

NOTES

check that user level systemd process is running

# you should see 'lib/systemd/systemd --user'
ps aux | grep systemd | grep "^$USER"

tried reinstalling dbus

sudo apt install --reinstall dbus
sudo apt install --reinstall dbus-user-session

checked env vars

id -u
id -un
echo $XDG_RUNTIME_DIR
echo $DBUS_SESSION_BUS_ADDRESS

tried enabling linger

loginctl enable-linger $USER

this showed errors, so restarted

# tried restart of login service
sudo systemctl restart systemd-logind.service

# then errors were shown in user system, so restarted
sudo systemctl status user@1000.service --no-pager -l
sudo systemctl restart user@1000.service

# then this worked
systemctl --user status