My personal website was due for an overall (and in many ways simplification) and I’ve been working around Nix long enough that I decided to build out my new site on it. Well, this is it!
At a high level, these are the tools I used:
- DigitalOcean for hosting the server (configured with Terraform)
-
A custom NixOS image which I found directions for here.
-
Flakes for configuration.
-
A flake for the hugo site, which I derived from here
Getting up to Speed
Before I could really get started, I decided to do a deeper dive into Nix. I had some working knowledge of Nix prior to this project so it was more a matter of filling in gaps. I specifically wanted to make sure I understood Flakes as they seemingly continue to grow in popularity. If you, like me, are just diving into Nix(OS) these should help you out as well!
The resources I used:
-
https://zero-to-nix.com/ from Determinate Systems for getting up and running.
-
https://nix.dev/tutorials/nix-language for getting a handle on the language. Lots of great resources here that I plan on referencing in the future.
-
Haskell Programming from First Principles Really just the first chapter or two here is super helpful, and you could probably find other tutorials. Functional programming is new to me so going over Lambda Calculus was very beneficial. I happened to have started this book just before this endeavor.
Hosting on Digital Ocean
There are two options for hosting a NixOS machine on Digital Ocean:
-
Use NixOS-Infect. A lot of people online seem to have success with this method but I lost SSH access to the box every time I ran the script despite having my keys appropriately staged on it. I’m not ruling this out as a method, I might have made mistakes but I found the second option to be the easier way forward.
-
Use a custom NixOS Image. Once you have the image, simply upload it to your Digital Ocean account and fire up the instance!
Configuration with Flakes
Below is my configuration.nix
file for this server. The only thing I modified here was nix.settings.experimental-features
and system.stateVersion
. The imports
were setup when I created the NixOS image and are what enables this image to work on Digital Ocean. They are best left alone!
Configuration.nix
{ modulesPath, lib, pkgs, ... }:
{
imports = lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [
(modulesPath + "/virtualisation/digital-ocean-config.nix")
];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
system.stateVersion = "23.11";
}
So there isn’t much in configuration.nix
because I’m using Flakes to manage this server. My flake.nix
is below.
Flake.nix
Notes
-
The
inputs
section calls all the flakes I use inoutputs
. Some of these are referenced usinggit+ssh
and others just use the relative file path. All my NixOS work lives in the same monorepo so I could have just stuck with relative filepaths for everything but I was experimenting. -
My only output is a
nixosConfiguration
calledsebsec
. -
Within that we have a handful of modules being imported. The first is my
configuration.nix
file from above, the next two are some flakes which define my users and standard packages for the machine. -
The last one is calling in some additional configuration which I was not able to setup within a flake. Specifically, it setups up
services.openssh.enable = true
and some other OpenSSH related settings. Apparently pulling in additionalnixosConfigurations
from other flakes isn’t quite supported as indicated by this wonderfully snarky error message:
While loading a configuration into the module system is a very sensible idea, it can not be done cleanly in practice.
-
Also note that it calls my
hugo-site
package (using thesystem
variable to ensure we grab the right version) which we will see defined below and in thengnix
settings points the root to where the package will live in/nix/store
. This is extremely important because each time changes are made, that filepath will change to reflect the hash of the new build. -
The last section does what it says, it sets up
nginx
and handles generating the SSL certificate. Realistically, I could move this to its own module file similar tobaseConfig.nix
in order to keep my mainflake.nix
as clean as possible. And of course, it opens up the required ports.
{
description = "flake for yourHostNameGoesHere";
inputs = {
nixpkgs = {
url = "github:NixOS/nixpkgs/nixos-23.11";
};
flake-utils.url = "github:numtide/flake-utils";
users-flake.url = "git+ssh://git@github.com/sjdwhiting/NixOS.git?dir=users";
users-flake.inputs.nixpkgs.follows = "nixpkgs";
packages-flake.url = "git+ssh://git@github.com/sjdwhiting/NixOS.git?dir=systemPackages";
packages-flake.inputs.nixpkgs.follows = "nixpkgs";
hugo-site.url = "../../packages/hugo";
hugo-site.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, users-flake, packages-flake, hugo-site, ... }: {
nixosConfigurations = {
sebsec = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = [
./configuration.nix
users-flake.nixosModules.users
packages-flake.nixosModules.defaultPackages
../../modules/baseConfig.nix
# Sets up the static website based on flake
({ pkgs, ... }: {
environment.systemPackages = with pkgs; [
hugo-site.packages.${system}.website
];
# Sets up NGINX and points it to the updated website.
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.nginx.enable = true;
security.acme.acceptTerms = true;
security.acme.defaults.email = <email>";
services.nginx.virtualHosts."sebsec.io" = {
enableACME = true;
forceSSL = true;
root = "${hugo-site.packages.${system}.website}";
locations."/".extraConfig = ''
try_files $uri $uri/ /index.html;
'';
};
})
];
};
};
};
}
Hugo Flake
Lastly, we have the flake.nix
for Hugo
which we imported in the flake.nix
above. What I appreciate about this is the simplicity, it made for a great way to get my hands on using flakes and achieve the goal of launching this site on NixOS.
Notes
Inputs
nixpkgs
hugo-coder
which is a theme. I’m not actively using that theme, nor am I using inputs for my theme at this time but I left it in as an example of how you can ensure your themes are always up to date using Nix.
Outputs
-
utils.lib.eachSystem
ensures this will build on multiple systems. Adjust based on your needs. -
We are building the package and a development shell. The build of the package is pretty straightforward, but I did have to modify it a bit and you can see it contains some debugging commands.
-
It links any themes you called as inputs into the
themes
folder of your hugo project -
It runs
hugo
to build the site -
In the
installPhase
it copies the results of runninghugo
, which is simply thepublic
folder, to$out
which is the/nix/store
path and what we referenced in the aboveflake.nix
.
-
The development shell allows you to easily work on the hugo site by either running
nix develop
while in the same directory as your flake or by usingdirenv
to automatically utilize the shell defined in the flake. direnv and Nix -
For some added context this is what my file structure looks like. Eventually, this could support multiple websites but for now you can see how the hugo site lives under the
sebsec
folder which is why in thebuildPhase
there is thatcd sebsec
command. Check out this quick start guide for getting up and running withhugo
File Structure
├── hugo
│ ├── flake.lock
│ ├── flake.nix
│ └── sebsec
│ ├── archetypes
│ ├── assets
│ ├── content
│ ├── data
│ ├── hugo.toml
│ ├── i18n
│ ├── layouts
│ ├── public
│ ├── resources
│ ├── static
│ └── themes
The actual flake
# To use a new theme:
# 1. Add the theme as an input
# 2. Symlink the theme to the themes directory
# 3. Update the hugo.toml file to use the new theme
# Alternativeliy, download and add the theme folder to sebsec/themes and updae hugo.toml
# This option allows you to customize the theme files if you desire.
{
description = "Personal website for Sebastian";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
utils.url = "github:numtide/flake-utils";
hugo-coder = {
url = "github:luizdepra/hugo-coder";
flake = false;
};
};
outputs = inputs@{ self, nixpkgs, utils, ... }:
utils.lib.eachSystem [
utils.lib.system.x86_64-darwin
utils.lib.system.x86_64-linux
utils.lib.system.aarch64-darwin
utils.lib.system.aarch64-linux
]
(system:
let
pkgs = import nixpkgs {
inherit system;
};
in
rec {
packages.website = pkgs.stdenv.mkDerivation {
name = "website";
src = self;
buildInputs = [ pkgs.git pkgs.nodePackages.prettier ];
# I've kept these debug statements because I've hadd occasional issues with the build phase and
# its proven valuable to have this info in the logs
buildPhase = ''
cd sebsec
echo "Currently in the following directory:"
pwd
ln -s ${inputs.hugo-coder} themes/hugo-coder
echo "themes folder currently contains:"
ls themes
echo "Running hugo -v "
${pkgs.hugo}/bin/hugo --logLevel info
'';
installPhase = ''
cp -r public $out
'';
};
packages.default = packages.website;
apps = rec {
hugo = utils.lib.mkApp { drv = pkgs.hugo; };
default = hugo;
};
devShells.default = pkgs.mkShell {
buildInputs = [ pkgs.git pkgs.nixpkgs-fmt pkgs.hugo ]; };
});
}
Conclusion
All in all, I’m happy with how things turned out. The site is easy to maintain now, especially with some GitHub actions setup that automate my deploys and the ability to run hugo server
to do local development. I made a lot of silly mistakes as I wrestled with Nix’s syntax but I’m in am much stronger place and ready to jump into my next Nix project! As I maintain this site, I already have some plans to clean up my configs further and make things more consistent but I wanted post everything as is right now in hopes that will help others on their Nix journey.