Non-root profile service deployment

I’d like to continuously deploy some software on a NixOS system with a separate cadence to the system profile. I can’t seem to find much about this, although I’ve seen it referred to in various posts, e.g. Home-manager is a false enlightenment mentions using nix-store --realise and nix-env --set.

I would want to be able to run some command on the server itself, ideally not as the same user that the service runs as, nor root.

Triggering is not a problem, I’d probably use a systemd path unit for this, watching the artefact output directory of the CI runner (laminar).

It seems as though deploy-rs has half the machinery that I want, namely multi-profile support (albeit without an activation function for this approach). I’m wondering what else I would need, or, alternatively, what other (simpler?) options there are, particularly since I don’t need to be able to do this remotely.

3 Likes

I’ve explored this some myself, but there’s no “tool” which provides this for you. At work we use a well known file path and a predefined systemd unit which points to the well know path, and we orchestrate a realize/restart. As far as i’ve found, at this point you’ll have to build it yourself.

1 Like

I am sure I am missing something, but you can always just call nix-build on any old nix expression and it will happily stick it in the the store, and leave a gc root at whatever path you like, no nix-env required.

1 Like

I think configuration is the main thing that makes this tricky for me, which I can’t see nix-build or nix-env providing, but I did just think that perhaps I can use makeWrapper to realise the configuration file path and associate it with the correct version. Time to experiment!

What do you need makeWrapper for? Assuming you have a ./foo.conf config file and pkg.nix as some function producing a derivation then you can make a default.nix like

let 
  pkgs = $pinnedPkgs;
  pkg = pkgs.callPackage ./pkg.nix {};
in
pkgs.writeShellScript "service" "exec ${lib.getExe pkg} --conf ./foo.conf"

where $pinnedPkgs is whatever you use to get nixpkgs in scope

then nix-build ./default.nix -o $path will drop an executable at $path that you can execute. I don’t think it takes too much more linux admin imagination to see how to package that into something that can update a service on some some trigger.

That’s… what makeWrapper does (but in a consistent format).

I personally find 10x easier to read a straightforwardly assembled shell script. This is also potentially me not understanding makeWrapper, but given that it is a setup hook you would need to do something like

pkgs.runCommand "wrapped" { buildInputs = [ pkgs.makeWrapper ]; } ''
  makeWrapper ${lib.getExe pkgs.hello} $out --add-flag "-g HAI"    
''

That doesn’t feel more obvious or straightforward.

Hmm, maybe that’s easier, actually. Just need to add "$@" to allow passing extra arguments and then it’s all fine.

I think I was a bit hung up on something I could run remotely as well, but I suppose that this will do for now.

If you want to build it remotely that’s fine, do that, and use nix-copy-closure + nix-store --add-root $stable_path -r $store_path, I haven’t tried exactly that, but I don’t see why it wouldn’t work. You’d just need to watch out for gc.

So with flakes this seems to be a bit more complicated. So far I’ve got this:

nix build \
    --out-link $ARCHIVE/hello \
    --inputs-from $WORKSPACE \
    --impure \
    --argstr name hello \
    --argstr system $(nix eval --impure --raw --expr 'builtins.currentSystem') \
    --argstr workspace $WORKSPACE \
    --file - <<-"EOF"
{ name, system, workspace }:
let
    pkgs = import (builtins.getFlake "nixpkgs") {
        inherit system;
    };
    inherit (pkgs) lib;
    self = builtins.getFlake workspace;
in
pkgs.writeShellScript name "exec ${lib.getExe self.packages.${system}.default} \"$@\""
EOF

Given that $WORKSPACE points to a flake with a default output for the current system. I had issues referencing a config files with ${./config}, claiming that /var could not be accessed in restricted mode, which, of course, is where my CI config is. For the first project I’m doing this with, I’ve made the default config work for my deployment, so I don’t need to specify the config file, but of course that’s not always feasible. I know that there’s other ways to do this, so I’m not too concerned.