diff --git a/content/blog/nix-unprivileged-deployments.md b/content/blog/nix-unprivileged-deployments.md new file mode 100644 index 0000000..fc4bf47 --- /dev/null +++ b/content/blog/nix-unprivileged-deployments.md @@ -0,0 +1,176 @@ +--- +title: Unprivileged deployments with Nix +author: me +date: 2025-04-17 +tags: nixos +--- + +Note: this post assumes familiarity with the Nix Ecosystem. + +Nix and NixOS make managing servers much easier than bespoke scripts or +complex Ansible playbooks. While I don't think NixOS is ready for +personal computers, it absolutely makes sense on less dynamic devices +like servers or embedded-ish machines. + +We can use NixOS and nixpkgs to derive a fully defined operating system and services. +Then, we can: + +- switch to new configurations live or on next boot +- build disk images for use as an installer or VM +- push the new configuration to a remote server using [deploy-rs]() + +The latter is interesting. With a bit of setup, we can do GitOps with rollback +and push-deployments. `deploy-rs` is a glorified `nix copy` with some extra magic +to "activate" the copied closure. Activation typically means running `home-manager switch` +or switching the NixOS profile. + +When starting this blog, I wanted to have it deployed automatically like the popular +SAAS options. This could be pretty simple if I wanted it to be: + +```bash +npm run build +scp -r _site/ blog@my-remote-server:_site/ +``` + +And then have `nginx` use `/home/blog/_site/` as the site root. This works, but doesn't feel +"Nix" enough. + +Other users have tied their blog to the NixOS system-level configuration, like [this user](https://jeancharles.quillet.org/posts/2023-08-01-Deploying-a-static-website-with-nix.html). While I think this +is a good solution, it has the issue of relying on root access for updating the site. +To me, this feels excessive. I should be able to deploy my site from a lesser-privileged user. + +# Scaffolding the Solution + +Let's recap: + +1. Build the site using a nix derivation +2. Copy the derivation to a server using a locked down non-root account. +3. Create well-known path that points to the latest version of the site in the nix store. +4. Configure `nginx` to serve from this well known path. + +I'll do it manually to get an idea of how it should work. + +## nix build blog + +I build my blog using [11ty](https://www.11ty.dev), and I use `npm run build` to generate +the static sources. Nix supports building npm-like packages with `buildNpmPackage`, which +uses a [Fixed-output derivation](https://nix.dev/manual/nix/2.28/store/derivation/outputs/content-address#fixed) +to store the dependencies of the project. Then, for the `installPhase`, we just copy the built contents +to `$out`. I need to add `vips` and `pkg-config` as well because 11ty processes my images. + +The end result looks like this: + +```nix +default = pkgs.buildNpmPackage { + name = "myblog"; + version = "unstable"; + buildInputs = with pkgs; [ + nodejs + vips + ]; + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + npmDepsHash = "sha256-Q7rhCjAPPn44DyUZ/uoD+7o4XH33IATfL+v1azEhuW0="; + src = ./.; + installPhase = '' + mkdir -p $out/public + cp -ar _site/ $out/public + ''; +}; +``` + +Then, running `nix build` I get my site files in `result/public`, which is what we want. +I put the files in a subdirectory so that we can use `buildEnv` to merge paths later without +adding random scripts to the site contents + +## Copy to server + +This is a little bit silly because the static site has no dependencies. We could just +use `scp` to copy the output over. `nix copy` goes further and also copies all the runtime +dependencies to the target, which is useful for actual programs or entire systems. +We can still use it for practice. + +``` +nix copy --to ssh://my-server '.#default' +``` + +When I log into my server, I can grep the store to find the blog: + +```bash +$ ls /nix/store | grep myblog +mqhssdlmg9f03avpajwcqaah2apknl02-myblog +``` + +Now I just need a symlink to this file, and a nginx vhost. I'll create a small NixOS +module that will set this up: + + +```nix +# TODO: write this +``` + +The last step is creating that symlink. This is where the concept of "activation" comes into play. +For NixOS, `deploy-rs` activation just calls `switch-to-configuration` to make the system change the profile. +We can effectively do whatever we want here. + +Reading the [custom activator](https://github.com/serokell/deploy-rs/blob/aa07eb05537d4cd025e2310397a6adcedfe72c76/flake.nix#L58C13-L96C17) source: +```nix +custom = + { + __functor = customSelf: base: activate: + final.buildEnv { + name = ("activatable-" + base.name); + paths = + [ + base + (final.writeTextFile { + name = base.name + "-activate-path"; + text = '' + #!${final.runtimeShell} + set -euo pipefail + + if [[ "''${DRY_ACTIVATE:-}" == "1" ]] + then + ${customSelf.dryActivate or "echo ${final.writeScript "activate" activate}"} + elif [[ "''${BOOT:-}" == "1" ]] + then + ${customSelf.boot or "echo ${final.writeScript "activate" activate}"} + else + ${activate} + fi + ''; + executable = true; + destination = "/deploy-rs-activate"; + }) + (final.writeTextFile { + name = base.name + "-activate-rs"; + text = '' + #!${final.runtimeShell} + exec ${final.deploy-rs.deploy-rs}/bin/activate "$@" + ''; + executable = true; + destination = "/activate-rs"; + }) + ]; + }; + }; +``` + + +This is a bit difficult to parse because there's the whole `__functor` bit. +Essentially, we use `buildEnv` to add some new scripts to the `base` package, +which are then used to call `activate`, which can be a shell script or a binary. + +I'll try a simple custom activation that creates a symlink. Of note is that +the `$PROFILE` variable points to the path of the `buildEnv` derivation. + +```bash +# remove link to old site. +rm -rf /var/lib/static-site/public +ln -sn $PROFILE/public /var/lib/static-site/public +``` + + + + diff --git a/styles/config/vocabularies/Base/accept.txt b/styles/config/vocabularies/Base/accept.txt index d57b185..cb32388 100644 --- a/styles/config/vocabularies/Base/accept.txt +++ b/styles/config/vocabularies/Base/accept.txt @@ -16,3 +16,6 @@ DIP SSOP QF[PN] + +nixpkgs? +NixOS