Building with Nix
This blog has been built with nix for a long time, but recently I revamped the infrastructure for building, massively simplifying everything and also decreasing build times to 20 seconds from 10 minutes. So I decided to write about it in case my setup was useful to others.
If you’ve never heard of nix, it is a package manager that is totally deterministic. I have a description of how to build my blog written in a file, and with nix I can be certain that wherever I build my blog, I will end up with exactly the same output files.
It does this using the nix programming language (I know, it’s confusing that both the package manager and the language are called nix). The nix programming language is a purely functional, turing complete language, used to create objects called derivations. These derivations describe how to build a certain piece of software, and derivations can depend on each other. The derivations are built in hermetic environments that only have access to the declared inputs, so if you forget to specify a dependency explicitly, it will not build.
My blog has two parts. One is a builder, which is a haskell package that depends on hakyll. I used the cabal2nix template from Practical Nix Flakes to describe how to build this haskell package.
The other part has the source files for my blog, which are mainly in markdown. This is in a private repo, because I don’t want drafts to be exposed, however you can access the current source here. I use the following flake.nix file to build this blog
{
description = "Owen's Blog";
inputs.nixpkgs.url = "github:nixos/nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.builder.url = "github:olynch/owenlynch.org-builder";
outputs = { self, builder, flake-utils, nixpkgs }:
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
let
pkgs = nixpkgs.legacyPackages.${system};
name = "owenlynch-org";
in {
packages.website = pkgs.stdenv.mkDerivation {
inherit name;
src = self;
buildInputs = [ builder.defaultPackage.${system} ];
LANG = "en_US.UTF-8";
LC_ALL = "en_US.UTF-8";
LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive";
buildPhase = ''
tar czf $TMPDIR/source.tar.gz .
mv $TMPDIR/source.tar.gz static/
${name} build
'';
installPhase = ''
mkdir -p $out/
cp -R _site/* $out/
'';
dontStrip = true;
};
defaultPackage = self.packages.${system}.website;
devShell = pkgs.mkShell {
buildInputs = [ builder.defaultPackage.${system} ];
};
hydraJobs.build = self.packages.${system}.website;
}
); }
The important part is the buildPhase and the installPhase, these run the build command from the builder package and then copies the resulting site into $out
, which is the directory that nix expects the output of a derivation to be in.
I then have a very simple github action
name: "Deploy"
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- name: Deploy with nix
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
echo -e "${{ secrets.DEPLOY_SSH_KEY }}\n" | ssh-add -
export SSHOPTS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ssh $SSHOPTS website-updater@proqqul.net "nix build github:olynch/owenlynch.org/$GITHUB_SHA --out-link /var/www/owenlynch.org/deployed"
All this does is ssh into my server (which was surprisingly difficult… I don’t know why it’s so hard to ssh from a github action), and runs nix build github:olynch/owenlynch.org/$GITHUB_SHA --out-link /var/www/owenlynch.org/deployed
, which builds the current commit of my website and symlinks the result to /var/www/owenlynch.org/deployed
. Then nginx serves that directory!
This is very fast, because the site builder gets built on my server, and then sticks around in the cache for the next time I want to rebuild my site. Previously, I was installing GHC and building my site builder on the github action, which was a massive waste of time.
Anyways, that’s it! The basic idea behind this would work for any static site generator, though for most static site generators it wouldn’t be such a big deal to split the builder from the source files because you don’t need 5G of haskell libraries to build the builder… Ah how I love and hate haskell…