systemd services and timers are a game changer if you’re used to cron jobs. This looks at a couple of simple real-life examples to show off the advantages.

From my /etc/nixos/configuration.nix:

{ config, lib, modulesPath, pkgs, specialArgs, options }: {
  systemd.services = {
    dynamic-dns-updater = {
      path = [
        pkgs.curl
      ];
      script = "curl https://example.org/?token";
      serviceConfig = {
        User = config.users.users.default.name;
      };
      startAt = "hourly";
    };

    sync-images = {
      environment = {
        DARKTABLE_PATH = pkgs.darktable;
      };
      path = [
        pkgs.procps
        pkgs.rclone
        pkgs.sqlite
      ];
      script = builtins.readFile ./sync-images.bash;
      serviceConfig = {
        User = config.users.users.default.name;
      };
      startAt = "daily";
    };
  };
  # Other stuff…
}

But how is this better than two lines in a crontab?

  • environment specifies variables as key/value pairs, so your command won’t be cluttered by them.
  • path defines the packages available on the $PATH. As far as I know only Bash is available by default. This avoids any kind of dependency on the rest of your system, so you can trivially do things like running a different version of Python. (If you are using the same package in environment.systemPackages or other services Nix only stores one copy of the package.)
  • serviceConfig.User links the service inextricably to a user declaration, so it’s easy to isolate jobs to a specific user.
  • startAt supports the more flexible and intuitive systemd time and date specification.
  • systemd services and timers log everything, so it’s easier to check whether your service is working: systemctl --user start dynamic-dns-updater and journalctl --unit=dynamic-dns-updater --user. Compare to having to change the cron job itself to set to a time shortly in the future, waiting for it to trigger, figuring out where your specific cron runner logs to, manually changing the cron job to the “production” value after checking that it works, and hoping you didn’t mess up the new timing.
  • systemd timers can tell you when they will run next: systemctl --user status dynamic-dns-updater.timer.
  • The script is part of the service derivation. So if the original script goes away your service won’t break.