So, `/etc/systemd/system/` is read-only... how do I enable template systemd units?

Hello,

So, I’m trying to monitor some paths with systemd to trigger systemd services that do things, so I made a systemd template like so:

  systemd.services."radicle-mirror@" = {
    description = "Mirror Radicle repository %I to remote git host";
    after = [ "radicle.service" ];
    wants = [ "radicle.service" ];

    script = ''
      set -euo pipefail

      RID="%I"
      MIRROR_FILE="/var/lib/radicle/mirrors.json"

      URL="$(jq -r '.["'"$RID"'"] // empty' "$MIRROR_FILE" 2>/dev/null || true)"
      [ -z "$URL" ] && exit 0

      cd "/var/lib/radicle/storage/$RID"

      export GIT_SSH_COMMAND="ssh -i /var/lib/radicle/ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=yes"

      echo "Mirroring $RID → $URL"
      git push --force --prune "$URL" refs/heads/*:refs/heads/* refs/tags/*:refs/tags/* 2>&1
    '';

    serviceConfig = {
      Type = "oneshot";
      User = "radicle";
      Group = "radicle";
      NoNewPrivileges = true;
    };
  };

  systemd.paths."radicle-mirror@" = {
    description = "Watch Radicle repository %I for changes";
    after = [ "radicle.service" ];
    wants = [ "radicle.service" ];

    pathConfig = {
      PathChanged = [
        "/var/lib/radicle/storage/%I/packed-refs"
        "/var/lib/radicle/storage/%I/HEAD"
      ];
      Unit = "radicle-mirror@.service";
    };
    wantedBy = [ "multi-user.target" ];
  };

however, if I attempt to enable the radicle-mirrors@.path, I’m greet with an error:

Enabling radicle-mirror@z39RJHSHs166S5kr8Qstj6kd1LFah.path...
Failed to enable unit: File /etc/systemd/system/radicle-mirror@z39RJHSHs166S5kr8Qstj6kd1LFah.path: Read-only file system

if my remote gets rebooted, I’m short of options to start those systemd path units.

is there a way to work around this without having to modify the NixOS configuration of my remote computer?

You’re not meant to use systemctl to enable units on NixOS, it has to be done declaratively.

What you should do is create a systemd service that sets overrideStrategy appropriately, since it’s a template unit.

1 Like

okay, but, what would be the effect of making that adjustment? would systemctl enable template-unit@someid.path work as expected?

overrideStrategy has nothing to do with it, and unfortunately your wantedBy isn’t doing what you want because all that does is set up a multi-user.target.wants/foo@.path -> ../foo@.path with no instancing.

What you probably should do is just systemd.targets.multi-user.wants = [ "radicle-mirror@instance-name.path" ];

I went with this solution: declaring a systemd unit service that will start all my .path units upon boot:

  systemd.services."radicle-mirror-starter" = {
    description = "Start radicle-mirror path units for all configured mirrors";
    before = [ "radicle.service" ];
    wantedBy = [ "multi-user.target" ];

    script = ''
      MIRROR_FILE="/var/lib/radicle/mirrors.json"

      if [ ! -f "$MIRROR_FILE" ]; then
        exit 0
      fi

      for RID in $(jq -r 'keys[]' "$MIRROR_FILE"); do
        echo "Starting radicle-mirror@$RID.path"
        systemctl start "radicle-mirror@$RID.path" || true
      done
    '';

    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = true;
      User = "radicle";
      Group = "radicle";
    };
  };

That is a lot more complicated than what I suggested and requires a whole extra state file :stuck_out_tongue:

the extra file (mirrors.json) is part of the design of what I’m trying to do, so it is not that extra!

If that file exists anyway for the purpose then you might just use a systemd generator.

systemd.generators.my-generator = (pkgs.writeShellApplication {
  name = "my-generator";
  runtimeInputs = [ jq ];
  text = ''
    MIRROR_FILE="/var/lib/radicle/mirrors.json"

    if [ ! -f "$MIRROR_FILE" ]; then
      exit 0
    fi

    mkdir -p $1/multi-user.target.wants
    for RID in $(jq -r 'keys[]' "$MIRROR_FILE"); do
      ln -s ../radicle-mirror@.path $1/multi-user.target.wants/radicle-mirror@$RID.path
    done
  '';
}) + "/bin/my-generator";

The reason I like this better is that it avoids having more runtime units in the graph cluttering things up. It just generates the dependencies that are needed on daemon-reload.