________________________________________________
|      _____________________________           |
| [][] _____________________________ [_][_][_] |
| [][] [_][_][_] [_][_][_][_] [_][_] [_][_][_] |
|                                              |
| [][] [][][][][][][][][][][][][][_] [][][][]  |
| [][] [_][][][][][][][][][][][][][] [][][][]  |
| [][] [__][][][][][][][][][][][][_] [][][][]  |
| [][] [___][][][][][][][][][][][__] [__][][]  |
|          [_][______________][_]              |
|______________________________________________|

            

This blog as a Nix derivation

Abandon all irreproducibility, ye who enter

Those past couple of months, I’ve been sucked deeper and deeper into the Nix wormhole. At its core, the idea of Nix is simple, it builds repeatable outputs from fixed inputs, from this simple concept comes a package and dev environment manager, an OS, and a DSL to write everything.

As I was thinking about the idea of starting this blog, I got drawn into the idea of writing it as a nix derivation. If it is then surely it can be written as an input of another derivation, the machine that runs the blog.

flowchart TB
    M[Machine that serves this site] --> B[Statically built blog] 
    B --> D[Optional build dependencies for the static blog] 

Building the blog

The blog is currently hosted on Github and can be found here. It’s a pretty classic statically compiled blog using hugo. I’m defining nix build files in a flake. They’re a relatively new feature in the nix ecosystem that are designed to increment reusability among various other things. Personnaly I like to use them to avoid rewriting nix build recipes: the nix description of how to build the blog reside along the blog code, it feels nicer.

The most important part of the flake is the blog derivation

blog = pkgs.stdenv.mkDerivation {
  inherit name;
  inherit version;
  src = ./.;

  buildPhase = ''
      cp -r $src/* .
      ${pkgs.hugo}/bin/hugo --config hugo.toml
  '';

  installPhase = ''
      mkdir -p $out
      cp -r public/* $out/
  '';
};

We define our blog as a derivation, one of nix’s core concepts. This derivation defines two phases:

Running nix build . produces a result directory that symlinks us to the build asset produced by nix, a directory in the store containing the hugo generated files.

Building the site

The site build recipes are hosted here. I’m running this blog from a machine that runs several services for me, with the idea that I might have to upgrade one day (which should be trivial now that the machine configuration is nixified).

The flake-configs project is quite dense and does a lot of things. I will just focus now how the blog is handled.

Specifying a dependency

Flakes allow us to specify other flakes as inputs, we’re doing that with:

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/release-23.11";
  flake-utils.url = "github:numtide/flake-utils";
  sops-nix = {
    url = "github:Mic92/sops-nix";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  blog = {
    url = "github:ldenefle/blog.lunef.xyz";
    inputs.nixpkgs.follows = "nixpkgs";
  };
};

Here we define the nixpkgs dependency first, that will let us use all the pkgs already defined by the nixpkgs project. Noticed that we pin the release 23.11, to make sure our dependencies don’t suddenly change.

Then, we define the blog dependency, and we make sure that the flake.nix in the blog project will use the same set of dependency, so we use the follows attribute to override the nixpkgs inputs in blog.

This lets us have a variable blog in the flake-configs flake file that will expose its outputs.

We can use the repl to explore a bit what the blog attributes actually is.

In flake-configs root directory we can open a repl with nix repl

Then we load the current flake with :lf . (read load flakes .)

The variables inputs is now available in the repl, the use of tabs is recommended to get auto-complete and dig around what the attribute expose. We can even build the blog directly by using :b inputs.blog.packages.x86_64-linux.default (the x86_64-linux might depend on your system).

After leaving the repl there is now a result directory which is the same hugo outputs that we built earlier.

Creating the system

Every machine that I manage have a configuration in the hosts directory. The machine that serves this blog is named sirocco and it’s configuration can be found here.

I use caddy to handle https certificate and reverse proxy my services. For the blog creating the system is as simple as

{ config, inputs, system, modulesPath, ... }:

let
  blog = inputs.blog.packages."${system}".default;
in {
  imports = [ "${modulesPath}/virtualisation/digital-ocean-image.nix" ];

  services.caddy = {
    enable = true;
    virtualHosts."blog.lunef.xyz".extraConfig = ''
      encode gzip
      file_server
      root * ${blog}
    '';

  };

  networking.firewall = {
    enable = true;
    allowedTCPPorts = [ 80 443 ];
  };

  networking.hostName = "sirocco";
}

I removed a lot of fat from the original machine configuration as it’s not required for the blog handling.

Several things happen here:

The machine is buildable as my flake specify it as a an output nixosConfiguration.

nixosConfigurations = {
      sirocco = mkSystemx86 [ ./hosts/sirocco ];
    };

To build the machine configuration, all I have to do is

nix build .#nixosConfigurations.x86_64-linux.sirocco.config.system.build.toplevel

Then push the output ./result directory to the machine using

nix copy --to ssh://${HOST} ./result

And rebuild the remote nixos with

ssh ${HOST} nix-env -p /nix/var/nix/profiles/system --set $(readlink ./result)
ssh ${HOST} /nix/var/nix/profiles/system/bin/switch-to-configuration switch

I added a deploy script in flake-configs to automate the task.