Repeated Tasks with Systemd Service/Timers on NixOS
Systemd provides an excellent suite of tools for automating repeated tasks. It
handles logging, dependency management, and scheduling. When combined with
NixOS, you get the added benefits of package isolation and declarative
configuration. I’ve used this as a robust framework for automating everything
from web scraping to offsite backups. Let’s walk through how you might be able
to utilize this with your existing scripts. We’ll start by create a script
called myscript.sh
with the contents:
echo "The date is $(date)"
We’ll update our /etc/nixos/configuration.nix
to create our service and timer
to call the script:
systemd.services.my-repeating-task= {
serviceConfig.Type = "oneshot";
path = with pkgs; [ bash ];
script = ''
bash /path/to/myscript.sh
'';
};
systemd.timers.my-repeating-task = {
wantedBy = [ "timers.target" ];
partOf = [ "my-repeating-task.service" ];
timerConfig = {
OnCalendar = "*:0/1";
Unit = "my-repeating-task.service";
};
};
It’s a good idea to keep my-repeating-task the same between the timer and the service. If they differ, additional config options are required so they can find each other.
We create a oneshot service to call our script and exit since we’re not spawning a process. The service will be called every minute by our timer. If you want to test right away, you can run the following after rebuilding to check the logs:
sudo systemctl start my-repeating-task.timer
sudo journalctl -fu my-repeating-task
That’s a good start. The thing about having to read logs, though, is that it’s tedious. We don’t want to monitor; we want results! We can send notifications utilizing ssmtp and a dummy email. You can easily use a Gmail or yahoo account but be aware that some hosts will require you to use an app password instead of your regular password.
services.ssmtp = {
enable = true;
root = "dummyuser@domain.com";
domain = "domain.com";
hostName = "smtp.mail.domain.com:587";
authUser = "dummyuser@domain.com";
# This needs to be an app password not your regular one
authPassFile = "/path/to/.ssmtp-authpass";
useTLS = true;
useSTARTTLS = true;
};
Then use sendmail to test that your SMTP settings are correct:
# If you don't have sendmail installed you can run temporarily install with
nix-shell -p system-sendmail
echo -e "To:me@domain.com\nFrom:dummyuser@domain.com\nSubject: hi\n\nbody text\n" | sendmail -t
Now the service can pipe the results of our script to sendmail to notify us. But what about service failures? Systemd can declare a service to run in the event of a failure. We’ll design a generic service that will receive the failed service’s name so it can email us with some relevant information:
systemd.services."notify-email@" = {
serviceConfig.Type = "oneshot";
path = with pkgs; [ systemd system-sendmail ];
scriptArgs = "%I";
script = ''
UNIT=$(systemd-escape $1)
TO="me@domain.com"
FROM="dummyuser@domain.com"
SUBJECT="$UNIT Failed"
HEADERS="To:$TO\nFrom:$FROM\nSubject: $SUBJECT\n"
BODY=$(systemctl status --no-pager $UNIT || true)
echo -e "$HEADERS\n$BODY" | sendmail -t
'';
};
systemd.services.my-repeating-service = {
serviceConfig.Type = "oneshot";
path = with pkgs; [ bash system-sendmail ]
script = ''
TO="me@domain.com"
FROM="dummyuser@domain.com"
SUBJECT="My first script"
HEADERS="To:$TO\nFrom:$FROM\nSubject: $S
BODY=$(bash /home/thorny/myscript.sh)
echo -e "$HEADERS\n$BODY" | sendmail -t
'';
onFailure = [ "notify-email@%n.service" ];
};
Our updated service will now call our notify service if anything fails during execution. To test both scenarios you can:
- execute the service as is. This will email the results of the script as expected.
- add
exit 1
to the end ofmyscript.sh
and then execute the service. This will send the error email.
That covers the basics of the automation setup. The final part is managing the packages for each service. NixOS will run the script in isolation, and it won’t have access to the packages you’ve installed. With the power of nix’s package management, we can specify different versions of each service’s packages. If you need different versions of PostgreSQL for two different backups you could do the following:
systemd.services.db1-backup= {
serviceConfig.Type = "oneshot";
path = with pkgs; [ postgresql_10 ]
script = ''
# command to backup
'';
onFailure = [ "notify-email@%n.service" ];
};
systemd.services.db2-backup= {
serviceConfig.Type = "oneshot";
path = with pkgs; [ postgresql_13 ]
script = ''
# command to backup
'';
onFailure = [ "notify-email@%n.service" ];
};
We can even utilize the packages of other channels such as unstable if we want to use the latest code:
systemd.services.job-using-unstable =
let
unstable = import <unstable> { config = config.nixpkgs.config; };
in
{
serviceConfig.Type = "oneshot";
path = with unstable; [ pkg1 ];
script = ''
# run stuff
'';
onFailure = [ "notify-email@%n.service" ];
};
TLDR; gimmie the code
services.ssmtp = {
enable = true;
root = "dummyuser@domain.com";
domain = "domain.com";
hostName = "smtp.mail.domain.com:587";
authUser = "dummyuser@domain.com";
# This needs to be an app password not your regular one
authPassFile = "/path/to/.ssmtp-authpass";
useTLS = true;
useSTARTTLS = true;
};
systemd.services."notify-email@" = {
serviceConfig.Type = "oneshot";
path = with pkgs; [ systemd system-sendmail ];
scriptArgs = "%I";
script = ''
UNIT=$(systemd-escape $1)
TO="me@domain.com"
FROM="dummyuser@domain.com"
SUBJECT="$UNIT Failed"
HEADERS="To:$TO\nFrom:$FROM\nSubject: $SUBJECT\n"
BODY=$(systemctl status --no-pager $UNIT || true)
echo -e "$HEADERS\n$BODY" | sendmail -t
'';
};
systemd.services.my-repeating-service = {
serviceConfig.Type = "oneshot";
path = with pkgs; [ bash system-sendmail ]
script = ''
TO="me@domain.com"
FROM="dummyuser@domain.com"
SUBJECT="My first script"
HEADERS="To:$TO\nFrom:$FROM\nSubject: $S
BODY=$(bash /home/thorny/myscript.sh)
echo -e "$HEADERS\n$BODY" | sendmail -t
'';
onFailure = [ "notify-email@%n.service" ];
};
systemd.timers.my-repeating-service = {
wantedBy = [ "timers.target" ];
partOf = [ "my-repeating-service.service" ];
timerConfig = {
OnCalendar = "daily";
Unit = "my-repeating-service.service";
};
};
This framework has now become the basis for all the personal tasks I automate. Adding new scripts is as easy as copying a few lines of code, as is moving an existing job to a different server. Using minimal external dependencies also means that I don’t have to worry about this solution becoming outdated for a long time. If you love automating all the small things, I highly recommend giving NixOS a spin.
Update (2022-08-03)
I’ve just updated from NixOS 21.11 to 22.05 and services.ssmtp
has been
replaced by programs.msmtp
. There are some minor tweaks from above. Most
notably, you can include the from address in the accounts section. If you do
not, sendmail -t
errors with envelope-from address is missing
which can be
solved by using sendmail -t --read-envelope-from
instead. It’s up to you.
programs.msmtp = {
enable = true;
defaults = {
tls = true;
port = 587;
};
accounts = {
default = {
auth = true;
from = "dummyuser@domain.vom";
host = "smtp.mail.domain.com:587";
passwordeval = "cat /path/to/.ssmtp-authpass";
user = "dummyuser@domain.com";
};
};
};
# Then the systemd parts don't require the FROM anymore
systemd.services."notify-email@" = {
serviceConfig.Type = "oneshot";
path = with pkgs; [ systemd system-sendmail ];
scriptArgs = "%I";
script = ''
UNIT=$(systemd-escape $1)
TO="me@domain.com"
SUBJECT="$UNIT Failed"
HEADERS="To:$TO\nSubject: $SUBJECT\n"
BODY=$(systemctl status --no-pager $UNIT || true)
echo -e "$HEADERS\n$BODY" | sendmail -t
'';
};