Deploying a static website with nix

Since I have discovered the existence of nix, I have slowly but surely nixified all the things around me: personal projects, machine configurations both on NixOS and on top of Ubuntu, my RPI, testing VMs and so on.

The last thing I needed to nixify was the deployment of this website. The project itself was already nixified a long time ago.

Have NixOS running on a VM

The first thing to do is to have NixOS running on a VM. I personally use Digital Ocean to host my website. One can create a NixOS image for Digital Ocean with a nix expression such as:

{ pkgs ? import <nixpkgs> { } }:
let
  config = {
    imports = [ <nixpkgs/nixos/modules/virtualisation/digital-ocean-image.nix> ];
  };
in
(pkgs.nixos config).digitalOceanImage

If you save this expression into a file called image.nix, then a call to

$ nix build --file ./image.nix

will build the image in the file ./result/nixos.qcow2.gz.

That file can then be uploaded to Digital Ocean and used to create a new VM. When you do so, make sure you select your SSH key to be able to connect to the VM once it is created.

Alternative: use nixos-infect

Create a derivation for the website

Now we need a derivation with the website content. The steps to achieve this are:

  • build the Hakyll executable
  • run it to make it create the actual webpages
  • get the result and put it in the nix store

Here are the relevant lines in flake.nix:

      overlay = (final: prev:
        {
          site = final.haskellPackages.callCabal2nix "site" ./. { };
          jeancharles-quillet = final.stdenv.mkDerivation {
            name = "jeancharles-quillet";
            src = ./.;

            LC_ALL = "C.UTF-8";

            buildInputs = [ final.site ];
            buildPhase = ''
              ${final.site}/bin/site build
            '';

            doCheck = true;
            checkPhase = ''
              ${final.site}/bin/site check
            '';

            installPhase = ''
              cp -r _site $out
            '';
          };
        });

This overlay defines two derivations:

  • site: the Hakyll executable, built with callCabal2nix
  • jeancharles-quillet: the webpages generated by the site program created by the previous derivation

Create the NixOS configuration

Now we need a NixOS configuration that setups a webserver which will serve our website content.

Check out this simple configuration.nix:

{ pkgs, modulesPath, ... }:
{
  console.keyMap = "fr";

  services.nginx.enable = true;
  services.nginx.virtualHosts."jeancharles.quillet.org" = {
    enableACME = true;
    forceSSL = true;
    root = "${pkgs.jeancharles-quillet}";
  };

  services.nginx.virtualHosts."quillet.org" = {
    globalRedirect = "jeancharles.quillet.org";
    enableACME = true;
    addSSL = true;
    serverAliases = [ "www.quillet.org" ];
  };

  security.acme.acceptTerms = true;
  security.acme.certs."jeancharles.quillet.org".email =
    "jeancharles.quillet@gmail.com";
  security.acme.certs."quillet.org".email = "jeancharles.quillet@gmail.com";

  networking.firewall.allowedTCPPorts = [ 22 80 443 ];
  services.sshd.enable = true;

  system.stateVersion = "22.11";
}

That is it, the full configuration of the VM:

  • we enable nginx service
  • use acme for getting a certificate for the domain names
  • we open the required ports
  • and finally point the root of the virtual host to the derivation that contains the pages themselves
  • also we enable the sshd service to be able to connect to the machine through SSH

Warp everything in the flake

All this is packaged together in this flake output:

      nixosConfigurations.website-prod = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ({ ... }: { nixpkgs.overlays = [ self.overlay ]; })
          ./configuration.nix
        ]
        ++
        (nixpkgs.lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [
          (nixpkgs + "/nixos/modules/virtualisation/digital-ocean-config.nix")
        ]);
      };

Deploy

Now deploying the website is as simple as:

$ nixos-rebuild switch --flake .#website-prod --target-host quillet.org

The configuration will be built on the local machine, then uploaded to the host and the relevant services will be restarted.

And that’s it! Now the webserver can be updated at any time with all the reproducibility guaranties that nix provides.

This is basically how this website is deployed. You can have a look at the details in the repository itself.

August 1, 2023