1
0
Fork 0

start new post on deployment
All checks were successful
Build Blog / Build (push) Successful in 3m36s

This commit is contained in:
Champlin, Saji 2025-04-17 12:59:03 -05:00
parent 5dc6aea4a1
commit b8b6e29643
2 changed files with 179 additions and 0 deletions

View file

@ -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
```

View file

@ -16,3 +16,6 @@ DIP
SSOP
QF[PN]
nixpkgs?
NixOS