Runing Repeated Jobs with systemd timers and NixOS
Systemd timers are a useful mechanism to create jobs on timers. NixOS offers solutions for conguring these job. Like everything NixOS, it’s takes a bit of time to get it just freaking work. I wrote this guide so you can get your jobs up and running faster.
My Introduction to systemd and NixOS
It started off as a simple problem. Run a python script to put some data in a Google Sheet on a daily basis. My first thought immediately, as someone who wants to create the simplest soluton is to setup a cron job.
At this point I had already begun my foray into NixOS. I figured that this small task would be a great way to test out Nix’s functionality for server usage. From there I began learning how to package up my python script to run as a Nix package and then configure it to run once a day. We’ll be talking about the later. I’ll write be another post for bundling the Python project as a Nix package.
Cron isn’t for me
First thing I figured I would do is dig through and figure out how to setup a Cron Job on NixOS. I went to the Cron nixos wiki and I read words every developer dreads to see DEPRECATED
. There was a suggestion on the page to use systemd timer
instead.
I had limited knowledge of systemd, but luckily recently I had watched YouTube video titled The Tragedy of systemd. Armed with existing knowledge, I thought “Looks like this systemd this is worth learning”.
Basic Configuration Setup
The first place I started was the NixOS systemd timer wiki. There it has a basic example but not a bunch of documentation explaining how the timers work.
Anotomy of a Timer
There are actullay 2 components that make up a timer. You need to define a systemd service and a systemd timer. I’ll break down the example used in the wiki for demo purposes.
1. The systemd Service
A service is a unit that is run by system. These are configured ahead of time. In our example we define a serivce that runs a script that echo
s “hello world”. We define it as a oneshot
type and give it a service name of hello-world
systemd.services."hello-world" = {
script = ''
set -eu
${pkgs.coreutils}/bin/echo "Hello World"
'';
serviceConfig = {
Type = "oneshot";
User = "root";
};
};
2. Systemd Timer
We define a timer with a few different configurations. In this case we give the timer a name of hello-world
and have it run 5 minutes after bootup. We also define a unit. This is the unit service that we defined earlier to run.
systemd.timers."hello-world" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnUnitActiveSec = "5m";
Unit = "hello-world.service";
};
};
The Quest for the Ultimate Configuration
The example in the wiki is pretty lackluster. Asside from not really explaining how systemd works, it does not tell you how to run your own package as a timer. Also, I want my timer to run once a day. Not some time after bootup.
Theres was more digging to do.
Getting A Working Configuration
After some trial an error with NixOS I found a way to get my Script working. I’m going to break down each step, to help grasp each Nix related concept. If you want you can just jump ahead to the complete configuration.
Step by Step
1. Import your package
I went ahead and defined a package that contained the Python script(s) that I wanted run. We have to go ahead and import them in our configuration.nix
.
imports = [
./python-package.nix
];
2. Define a helper variable.
Note that in this example our package has a package name of pythonApplication
. We can go ahead and import that package into a nice variable that’s easy to use.
pythonApplication = pkgs.callPackage ./vineglue-app.nix {};
3. Write our systemd service
Now that we imported our package and have a variable, we can go ahead and define the systemd service unit and the timer unit.
systemd.services = {
pythonApplication-task = {
serviceConfig.Type = "oneshot";
script = ''
${pythonApplication}/bin/main.py
'';
};
};
systemd.timers = {
pythonApplication-task = {
wantedBy = [ "timers.target" ];
partOf = [ "pythonApplication-task.service" ];
timerConfig = {
OnCalendar = "*-*-* 12:00:00";
Persistent = true;
Unit = "pythonApplication-task.service";
};
};
};
Complete Config
Putting it all together complete configuration.nix
file should like something like the following.
imports = [
./python-package.nix
];
pythonApplication = pkgs.callPackage ./vineglue-app.nix {};
systemd.services = {
pythonApplication-task = {
serviceConfig.Type = "oneshot";
script = ''
${pythonApplication}/bin/main.py
'';
};
};
systemd.timers = {
pythonApplication-task = {
wantedBy = [ "timers.target" ];
partOf = [ "pythonApplication-task.service" ];
timerConfig = {
OnCalendar = "*-*-* 12:00:00";
Persistent = true;
Unit = "pythonApplication-task.service";
};
};
};
Clean Reusable Configuration
This is how you define a configuration file that runs a nix pkg as a systemd service and timer.
Defining a new file
{ config, lib, pkgs, ... }:
let
# The package itself. It resolves to the package installation directory.
pythonApplication = pkgs.callPackage ./python-package.nix {};
# An object containing user configuration (in /etc/nixos/configuration.nix)
cfg = config.services.pythonApplication;
in {
# # Create the main option to toggle the service state
options.services.pythonApplication.enable = lib.mkEnableOption "pythonApplication";
# # Everything that should be done when/if the service is enabled
config = lib.mkIf cfg.enable {
systemd.services = {
pythonApplication-task = {
serviceConfig.Type = "oneshot";
script = ''
${pythonApplication}/bin/main.py
'';
};
};
systemd.timers = {
pythonApplication-task = {
wantedBy = [ "timers.target" ];
partOf = [ "pythonApplication-task.service" ];
timerConfig = {
OnCalendar = "*-*-* 12:00:00";
Persistent = true;
Unit = "pythonApplication-task.service";
};
};
};
};
}
This will run the python application script every day of the week at 12:00pm.
Enable Package
If you wrote your script correctly, there should be a flag to enable it. Let’s go ahead enable it.
services.pythonApplication.enable = true
Getting Started with Your First Timer
Once I got over the initial learning curve, I actually really enjoyed working with these types of timers. I realized that being able to see the time configruation from a single nix
file was major win. In general, these systemd configruations can be sprawling on other distributions.
So if you’re looking for a linux distribution to run your scheduled task, I would seriously consider NixOs. If you like this type of content, you may be interested in learning how to package a Python project in NixOS.