Improved Deploying Hugo to Production Using Nixos

This article builds on my first article where I deployed hugo using NixOS. There was still room for improvement with this process so I decided to make it more nix-y and have the deployment steps incorporated into the system configuration so when the machine hosting the website is built the website is rebuilt too.

In my initial hugo setup I had to run the hugo command to generate the static HTML/CSS files and then run rsync to copy them to the directory being served by nginx. In this improved setup these manual steps are wrapped into a systemd unit and the hugo config is checked into it’s own git repo too.

To begin I created a new github repo which contained the source code for my hugo website as well as the git submodule information pointing to the risotto hugo theme I’m using. I also created a .gitignore file containing one line “public/” to ignore the public directory generated by the “hugo” command. This directory is what the nginx web server serves and doesn’t make much sense checking into version control so I excluded it.

Once the repo is ready I could then create the nix website config. Since I already have a nginx reverse proxy I decided it’s easier to just have the same server serve my static files (as oppose to having a seperate server just to host static HTML/CSS files).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{ pkgs }: let

website-root = "/var/www/megacorp.industries";

hugo-website = pkgs.stdenv.mkDerivation {
  name = "hugo-website";

  src = pkgs.fetchFromGitHub {
    owner = "rapture-mc";
    repo = "hugo-website";
    rev = "e4a87ad3ed7ed4ec81849c04e5eb2b1f55c648ee";
    hash = "sha256-s3l6WmJwc30W8SnHLuXjGKItbERtTwVbELNN9furDq8=";
  };

  installPhase = ''
    mkdir $out

    ${pkgs.hugo}/bin/hugo

    cp -rv public $out
  '';
};
in {
  services.nginx.virtualHosts = {
    "megacorp.industries" = {
      forceSSL = true;
      enableACME = true;
      root = website-root;
    };
  };

  systemd.services.rebuild-website = {
    enable = true;
    description = "Rebuilds hugo website";
    script = ''
      if [ ! -d ${website-root} ]; then
        echo "Website directory doesn't exist, creating..."
        mkdir -p ${website-root}

        echo "Setting permissions on newly created directory..."
        chown nginx:nginx ${website-root}
      fi

      ${pkgs.rsync}/bin/rsync -avz --delete ${hugo-website}/public/ ${website-root}
      chown -R nginx:nginx ${website-root}
    '';
    unitConfig.Before = "nginx.service";
    wantedBy = ["multi-user.target"];
  };
}

The above code will:

  1. Download the hugo website git repo using fetchFromGitHub
  2. Generate the static files using the “hugo” command
  3. Create the /var/www/megacorp.industries directory if it doesn’t already exist
  4. Copy the generated static files to the /var/www/megacorp.industries directory
  5. Set the approrpiate permissions on the directory so nginx can serve the files

The main problem with this solution though is getting the hash value for the function fetchFromGitHub. To find the hash I first have to set it to an empty string which will then make the rebuild fail but subsequently output the correct hash. Since I’m using a GitOps approach this requires 2x commit’s (one commit with an empty string for the hash then a second commit with the actual hash) where 1x should suffice.

Also note that the above code is a snippet of my full reverse proxy configuration, so things like “services.nginx.enable = true” + “security.acme.acceptTerms = true” are missing and should be included if you’re going to use this code. See my github repo for the full configuration.

Megacorp Industries

Megacorp Logo Linux/NixOS enthusiast who writes and also provides Linux services!


2025-05-31