How do I add a bash script to the system so that I can call it via cron?

I have searched far and wide, and I must not be searching the right words.

I have found other topics like:

And a few others that only have the answer “use systemd.timer”

To replicate cron, including emailing output on failure, that’s a lot more complicated, than just using cron, which has all that packaged around it.

I’m fine using cron, and I can see how to set up a crontab, but what I can’t figure out is how to add a single file to the system.

I want a /path/to/backup_script.sh, I only care that it can be executed. In other distros, I’d put these things in /opt or have a dedicated directory somewhere for backup scripts.

How do I take some bash script that I can add to my nix store, and then get it into the real system?

Forgive me for not having the right words to describe this, I’ve only been using NixOS for like a month or two.

1 Like

Well, you can create a derivation for it!
In the good old stdenvNoCC.mkDerivation { . . . } style. The only real difference is that src would point to your shell script, something like src = ./path/of/doom.sh;

Since, for such small custom scripts, stdenv is too huge, you can pick one of Nixpkgs’ trivial builders. Here is the documentation about:

https://nixos.org/manual/nixpkgs/stable/#part-builders
https://nixos.org/manual/nixpkgs/stable/#chap-trivial-builders
https://nixos.org/manual/nixpkgs/stable/#trivial-builder-writeText

Maybe what you want is writeShellScriptBin.

4 Likes

You can use something like this configuration module:

{config, pkgs, lib, ...}: {
  services.cron = 
    let 
      backupScript = pkgs.writeShellScript "backup-script.sh" ''
        # do your backup 
      '';
      # it's also possible to load something external but you should use a portable
      # shebang like `#!/usr/bin/env bash`
      # backupScript = pkgs.writeSript "backup-script.sh" builtins.readFile ./backup-script.sh # local to the config
    in {
      enable = true;
      systemCronJobs = [ 
        "* * * * *  root ${backupScript}"
      ];
    };
}
5 Likes

Even better specifically for the case of bash scripts is writeShellApplication: It sets up all options you’d want in a reasonably robust script and checks it using shellcheck.

4 Likes

Thank you all so much for this! This puts together all the things I couldn’t figure out on my own!

2 Likes

This probably won’t be an issue if your backup script is ~trivial, but if it’s extensive it’s worth being aware that this (requiring shellcheck) can mean having to update the script to fix new errors when a nixpkgs update includes a shellcheck update.

The replies above don’t directly address this (though writeShellApplication will get you on the right track), but a common stumbling block is environment differences for scheduled services/tasks (be they init system or cron).

I can elaborate a bit more if your script(s) are complex, but I’ll save the typing unless you ask :slight_smile:

3 Likes

I am using the shellcheck path, and it’s handy.

One thing I am finding is that the cron job template isn’t quite right with ${backupScript} it seems to need a bit more

  services.cron = let
    scriptContent = builtins.readFile ./wordpress_backups.sh;
    backupScript = pkgs.writeShellApplication {
      runtimeInputs = [pkgs.borgbackup];
      name = "wordpress_backup.sh";
      text = scriptContent;
      executable = true;
    };
  in {
    enable = true;
    systemCronJobs = [
      "0 0 * * * root ${backupScript}"
    ];
  };

my cron entry contains:

[root@wordpress:/mnt/backup]# cat /etc/crontab
SHELL=/nix/store/m0s1xf30bdk6vfn5m6c3mhb2z8w1cib5-bash-5.2-p15/bin/bash
PATH=/nix/store/z8jmrgzmm6dc8j4xb2f40nbaf1psn31n-system-path/bin:/nix/store/z8jmrgzmm6dc8j4xb2f40nbaf1psn31n-system-path/sbin

NIX_CONF_DIR=/etc/nix
0 0 * * * root /nix/store/ij1s32w632z0wvbcs2flh3zcs0s10z01-wordpress_backup.sh

but /nix/store/ij1s32w632z0wvbcs2flh3zcs0s10z01-wordpress_backup.sh is a directory containing /bin/wordpress_backup.sh

I assume there’s a property I need to use on the end like ${backupScript.bin} or something? How would I look that up?

1 Like

I ultimately solved it by doing this:

  services.cron = let
    scriptContent = builtins.readFile ./wordpress_backup.sh;
    backupScript = pkgs.writeShellApplication {
      runtimeInputs = [pkgs.borgbackup];
      name = "wordpress_backup.sh";
      text = scriptContent;
    };
  in {
    enable = true;
    systemCronJobs = [
      "0 0 * * * root ${backupScript}/bin/wordpress_backup.sh"
    ];
  };

This works, is there a better way to do it?

1 Like

Correct,
writeShellApplication writes to /bin, like writeScriptBin and writeShellScriptBin… I find them more useful if then I’ve to include the /bin directories of a bunch of packages with lib.makeBinPath or pkgs.buildEnv or as inputs to other packages…

2 Likes
lib.getExe backupScript
4 Likes