I recently learnt in home-manager (Nix) you can run systemd services as your own user. This is nice because we don’t need “sudo” permissions to do so. I also prefer to have as much of my config as possible in home-manager, again I don’t need to run “sudo”. Which is probably safer running apps in the least privileged mode.

In my case, I wanted to run an attic, a binary cache, watch store command which uploads any changes to /nix/store to my binary cache. Previously I had this running as a systemd service running as root i.e. managed my NixOS. However, I wanted to run the same service, on my non-NixOS machine. So I decided to have a look and see if I could run it in my home-manager config so I could run it the same way across all my machines. Whilst researching I came across the systemd.user.services option which allows us to do exactly this.

You can see how I set it below:

{
  systemd.user.services.attic-watch-store = {
    Unit = {
      Description = "Push nix store changes to attic binary cache.";
    };
    Install = {
      WantedBy = [ "default.target" ];
    };
    Service = {
      ExecStart = "${pkgs.writeShellScript "watch-store" ''
        #!/run/current-system/sw/bin/bash
        ATTIC_TOKEN=$(cat ${config.sops.secrets.attic_auth_token.path})
        ${pkgs.attic}/bin/attic login prod https://majiy00-nix-binary-cache.fly.dev $ATTIC_TOKEN
        ${pkgs.attic}/bin/attic use prod
        ${pkgs.attic}/bin/attic watch-store prod:prod
      ''}";
    };
  };
}

One thing I liked was I didn’t need to create an extra binary/shell script for ExecStart to run. i.e. ExecStart = watch-store.sh. We can simply create a bash script inline and give it a name. home-manager will work out creating this file and updating the systemd config to point to it for us. One fewer file in our config.

In the example above this is done using the pkgs.writeShellScript function we must provide it with a name (of the file) i.e. watch-store and then the contents of the file itself.

After running home-manager switch we should be able to see our service running (ignore the fact mine is failing).

The command systemctl --user status attic-watch-store.service could produce the following:

systemctl --user status attic-watch-store.service
× attic-watch-store.service - Push nix store changes to attic binary cache.
     Loaded: loaded (/home/haseebmajid/.config/systemd/user/attic-watch-store.service; enabled; vendor preset: enabled)
     Active: failed (Result: exit-code) since Sat 2023-10-07 23:38:48 BST; 11h ago
    Process: 1920392 ExecStart=/nix/store/2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store (code=exited, status=1/FAILURE)
   Main PID: 1920392 (code=exited, status=1/FAILURE)
        CPU: 85ms

Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920443]:     0: error trying to connect: dns error: Devic>
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920443]:     1: dns error: Device or resource busy (os er>
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920443]:     2: Device or resource busy (os error 16)
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920489]: Error: error sending request for url (https://ma>
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920489]: Caused by:
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920489]:     0: error trying to connect: dns error: faile>
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920489]:     1: dns error: failed to lookup address infor>
Oct 07 23:38:48 nix 2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store[1920489]:     2: failed to lookup address information: Tem>
Oct 07 23:38:48 nix systemd[2551]: attic-watch-store.service: Main process exited, code=exited, status=1/FAILURE
Oct 07 23:38:48 nix systemd[2551]: attic-watch-store.service: Failed with result 'exit-code'.

We can also find the systemd file in ~/.config/systemd/user/attic-watch-store.service. Which may look something like:

bat attic-watch-store.service --plain
[Install]
WantedBy=default.target

[Service]
ExecStart=/nix/store/2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store

[Unit]
Description=Push nix store changes to attic binary cache.

Where the ExecStart file looks something like this:

bat /nix/store/2z87s2lr68c6vwivphv0hp3nscgqfga6-watch-store --plain
#!/nix/store/xdqlrixlspkks50m9b0mpvag65m3pf2w-bash-5.2-p15/bin/bash
#!/run/current-system/sw/bin/bash
ATTIC_TOKEN=$(cat %r/secrets/attic_auth_token)
/nix/store/sm7wscbpxv4nsxdv7bik39skll81fy5i-attic-0.1.0/bin/attic login prod https://majiy00-nix-binary-cache.fly.dev $ATTIC_TOKEN
/nix/store/sm7wscbpxv4nsxdv7bik39skll81fy5i-attic-0.1.0/bin/attic use prod
/nix/store/sm7wscbpxv4nsxdv7bik39skll81fy5i-attic-0.1.0/bin/attic watch-store prod:prod

That’s about it! How you can manage systemd services via home-manager running as your own user.