Create a static blog with Nix
• Published: • Updated:This blog has been written with Eleventy. It handles the static site generation part, like converting markdown to HTML, routes and so on. Classic static site generator stuff. Their slogan is "Eleventy is a simpler static site generator" and so far I think it lives up to it.
What I don't like about it is that you need to install Node.js on your machine to get it to work. I seriously considered using Zola instead for this reason alone. In the end I decided for Eleventy because it's more mature and has a bigger community. The documentation is also more complete I feel.
If you want to skip ahead and take a look at the resulting repository, you can find it at tech-blog/create-a-static-blog-with-nix.
Also, there is also a continuation of this post in Caching adventures with Caddy and Nix. It's where we investigate the (tricky) caching issues with this setup and fix them.
Bootstrapping the project
So, given that I decided to go with Eleventy, that means we need to have Node.js installed. I don't want to have it installed system-wide just for Eleventy to work, so I decided to reach for the power of Nix.
To be fair, I would've used Nix even if I went with some other static site generator, but using something like Eleventy means I get to learn how to use Node packages with Nix.
First step is creating a git repository (git init) and a new flake (nix flake init).
I like to use flake-utils, so I add that to the inputs and adjust the flake to use it.
Run nix flake update to get the flake.lock file and then let's do an initial commit (git add . && git commit -m "Initial commit").
Now we have a good starting point.
Commit: Initial commit
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
...
}@inputs:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [];
shellHook = "";
};
}
);
}
Development shell
Next step I always do is add a development shell.
If you're not familiar with that, here is a nice write up (I will use nix develop).
But the short summary is that it provides a sort of virtual environment with the tools you need to interact with the project.
It is super simple for other people to use and I think it is way better than using docker/podman for sharing development environments.
I just can't live without just, the task runner. I always use it to run tasks in the project, but not everyone has it installed.
# This command will output the path of the just command if you have it installed
❯ which just
/Users/luka/.nix-profile/bin/just
You can see I already have it installed system-wide, but I always try to include all project dependencies in my development shell, just in case anyone else wants to interact with the project.
Let's modify the flake.nix to add it.
diff --git a/flake.nix b/flake.nix
index 58dadad..fffb25c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -18,7 +18,9 @@
in
{
devShells.default = pkgs.mkShell {
- buildInputs = [];
+ buildInputs = with pkgs; [
+ just
+ ];
shellHook = "";
};
}
Now, we have to activate the development shell.
We can do that by running nix develop.
❯ nix develop
[...]
❯ which just
/nix/store/w0s0f89zcpwl9ipygxiihba0j6fgwnyq-just-1.40.0/bin/just
❯ #press CTRL+D to exit this development shell or run exit command
exit
❯ which just
/Users/luka/.nix-profile/bin/just
As you can see above, we get a just binary, that is exactly defined by the nixpkgs commit our flake.lock is pinned to.
When you update the flake (nix flake update) your project binaries will also update.
In my case the binary provided by the system and the one provided by the development shell one are the same, but that will not always hold.
One thing you might've noticed if you're using zsh is that the new shell is pure bash.
Use nix develop -c $SHELL to retain your shell instead.
Anyway, now, when you want to work on your blog/project, you need to cd into the project root and run nix develop.
If you're using the excellent direnv, you can also paste use flake in your .envrc, run direnv allow and now whenever you cd into this repo, you'll get dropped into the development shell automatically.
Now let's run just --init to create a justfile and edit to something like:
# The default command to run when ran with just 'just'
[group('General')]
default: help
# Print the available commands
[group('General')]
help:
@just --list
# Update project dependencies
[group('General')]
update:
nix flake update
Now when you run just you get a nice list of available recipes you can choose from.
❯ just
Available recipes:
[General]
default # The default command to run when ran with just 'just'
help # Print the available commands
update # Update project dependencies
I think it's time we commit these changes and work on getting Eleventy into our development shell.
Commit: Add just and justfile
Add Eleventy
Now here comes the tricky part. We need to add Eleventy to our project dependencies.
As mentioned earlier, I don't really want to install Node.js to my machine, so let's see if we can find Eleventy in Nixpkgs repository and use that instead. Searching nixpkgs for the Eleventy package, we find nothing (as of 2025-06-22 at least). What now?
Well, we can package it ourselves and add it to our repository as a dependency that way. This being Nix, it took me quite a while to figure out how to do all of this. But I persevered, because that is what I do and I like doing things the hard way I guess. Anyway after a few hours of searching the web and scouring the nixpkgs source code here is what I came up with.
I found the pkgs.buildNpmPackage function (source).
It seems like a perfect candidate for us to use.
To build the NPM package it needs its source code, so let's get that first.
You do that by adding it as an input to our flake. All inputs are git repositories and Nix manages them for us.
diff --git a/flake.nix b/flake.nix
index fffb25c..18aea66 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,6 +2,11 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
+ eleventy-src = {
+ url = "github:11ty/eleventy";
+ flake = false;
+ };
+
};
outputs =
Since the Eleventy repository does not contain a flake, you need to set flake = false;.
Now let's pass that to buildNpmPackage to build it for us.
Let's create a pkgs folder and place a 11ty-eleventy.nix file there with the following contents:
# We'll pass in the pkgs and `src`
{ pkgs, src }:
pkgs.buildNpmPackage {
# Package name
pname = "eleventy";
version = "1.0.0";
src = src;
npmDepsHash = pkgs.lib.fakeHash; # We'll replace this soon
dontNpmBuild = true;
meta = with pkgs.lib; {
description = "A simpler static site generator";
homepage = "https://www.11ty.dev/";
license = licenses.mit;
};
}
We set dontNpmBuild to true because the package.json in the Eleventy repository does not contain a build script.
And use it in our flake.nix:
diff --git a/flake.nix b/flake.nix
index 18aea66..58dd8c7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,6 +20,10 @@
system:
let
pkgs = nixpkgs.legacyPackages.${system};
+ eleventy = import ./pkgs/11ty-eleventy.nix {
+ inherit pkgs;
+ src = inputs.eleventy-src;
+ };
in
{
devShells.default = pkgs.mkShell {
Make sure that you stage the changes with git (git add .), otherwise you'll get an error that [...]source/pkgs/pkgs/11ty-eleventy.nix' does not exist when Nix tries to use it.
Cool, now we have everything set up, all we need is to expose it in our outputs.
diff --git a/flake.nix b/flake.nix
index 58dd8c7..e45810f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -26,6 +26,7 @@
};
in
{
+ packages.eleventy = eleventy;
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
just
nix flake show command shows us the outputs of our flake.
I've edited it for brevity a bit, but yours should look similar.
We can see that we have one devShell (default) and one package (eleventy).
❯ nix flake show
warning: Git tree '[...]/create-a-static-blog-with-nix' is dirty
git+file://[...]/create-a-static-blog-with-nix
├───devShells
│ ├───aarch64-darwin
│ │ └───default: development environment 'nix-shell'
[...]
└───packages
├───aarch64-darwin
│ └───eleventy: package 'eleventy-1.0.0'
[...]
Now let's try and build it.
❯ nix build .#eleventy
warning: Git tree '[...]/create-a-static-blog-with-nix' is dirty
error: hash mismatch in fixed-output derivation '/nix/store/kx4k4fh0hh7v0apnxc9xrnca6g71nwrh-eleventy-1.0.0-npm-deps.drv':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-LGdCM1gjt3hRn7BiIlbA4e2HOiQ6e/qkAtWp0Qwn+PE=
error: 1 dependencies of derivation '/nix/store/cbc08ym28sgzz123gjf06q0k26j2sxc1-eleventy-1.0.0.drv' failed to build
If you've followed along this is what you should see, just the hashes will be different.
We expected this error to happen, since we used a fakeHash.
If we replace the fake hash with the correct one the package should build.
IMPORTANT: You should use the hash that you got, don't copy mine. As you'll get another hash mismatch.
diff --git a/pkgs/11ty-eleventy.nix b/pkgs/11ty-eleventy.nix
index c0692bf..c6adbfa 100644
--- a/pkgs/11ty-eleventy.nix
+++ b/pkgs/11ty-eleventy.nix
@@ -5,7 +5,7 @@ pkgs.buildNpmPackage {
pname = "eleventy";
version = "1.0.0";
src = src;
- npmDepsHash = pkgs.lib.fakeHash; # We'll replace this soon
+ npmDepsHash = "sha256-LGdCM1gjt3hRn7BiIlbA4e2HOiQ6e/qkAtWp0Qwn+PE=";
dontNpmBuild = true;
meta = with pkgs.lib; {
description = "A simpler static site generator";
Running nix build .#eleventy now completes successfully and gives us a result file in the root of our repo.
If we take a look at what it points to, we can see it points at /nix/store/[...]-eleventy-1.0.0.
❯ readlink result
/nix/store/hqa8c57nbl7rlimi1fa611r64lp0mp4w-eleventy-1.0.0
And taking a deeper look at the result, we can see that it contains a binary (result/bin/eleventy) and node_modules.
❯ tree result | head
result
├── bin
│ └── eleventy
└── lib
└── node_modules
└── @11ty
└── eleventy
├── cmd.cjs
├── CODE_OF_CONDUCT.md
├── LICENSE
Let's try executing that binary:
❯ ./result/bin/eleventy --version
3.1.2-beta.2
❯ ./result/bin/eleventy --help | head
Usage: eleventy
eleventy --input=. --output=./_site
eleventy --serve
Arguments:
--version
--input=.
Input template files (default: `.`)
Works like a charm! What a great moment.
To get that binary available in our development shell, we need to add the derivation (what Nix calls packages) as a build input.
Nix will then add the bin folder of that derivation to our path.
diff --git a/flake.nix b/flake.nix
index e45810f..29adafc 100644
--- a/flake.nix
+++ b/flake.nix
@@ -30,6 +30,7 @@
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
just
+ eleventy
];
shellHook = "";
};
Let's enter the shell and try it out:
❯ nix develop
❯ eleventy --version
3.1.2-beta.2
❯ which eleventy
/nix/store/hqa8c57nbl7rlimi1fa611r64lp0mp4w-eleventy-1.0.0/bin/eleventy
We can see it's exactly the same Nix store path as the one we got when we ran nix build .#eleventy.
We could call it quits at this point, but I don't like that the version we specified in the package does not match the actual eleventy version.
Another thing that bothers me is that when the eleventy repository updates (and we run nix flake update to get that update), we'll have to manually update the hash to the new one.
Ugh, manual work! I would much rather invest a few more hours into this right now, then ever have to worry about manual work.
Relevant XKCD as always: Is it worth the time?. In my case I think the answer would be a no, but hopefully for you it might be a yes.
Anyway, I had to figure out how this hash is calculated.
Turns out there is a package called prefetch-npm-deps that given a package-lock.json file outputs the desired hash.
So let's use it to calculate the hash.
Let's create a new justfile recipe called update-eleventy and let's not forget to add the binaries we use in the recipe to our development shell.
diff --git a/flake.nix b/flake.nix
index 29adafc..dd6cc6d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,6 +31,9 @@
buildInputs = with pkgs; [
just
eleventy
+ prefetch-npm-deps
+ jq
+ curlMinimal
];
shellHook = "";
};
diff --git a/justfile b/justfile
index 6776929..c7450cb 100644
--- a/justfile
+++ b/justfile
@@ -11,3 +11,13 @@ help:
[group('General')]
update:
nix flake update
+
+# Update packages sha256 and version
+update-eleventy:
+ #!/bin/bash
+ pkgs=$(pwd)/pkgs
+ cd $(mktemp -d)
+ curl -f https://raw.githubusercontent.com/11ty/eleventy/refs/heads/main/package-lock.json -o package-lock.json
+ prefetch-npm-deps package-lock.json > $pkgs/11ty-eleventy.sha256
+ cat package-lock.json | jq --raw-output ".version" > $pkgs/11ty-eleventy.version
+
Let me explain the update-eleventy recipe, line by line:
#!/bin/bash- this tells Just to interpret this recipe as a bash script
- this is significant, since otherwise Just executes each line in its own separate shell, a
cdwould have no effect for example
pkgs=$(pwd)/pkgs- set a
pkgsvariable to$(pwd)/pkgs pwdstands for 'print working directory', which outputs the absolute path of the current directory- we'll need this variable to copy the files back to our repository
- set a
cd $(mktemp -d)mktemp -dcreates a temporary directory and outputs its path- so we
cdinto a temporary directory that we created - we do this, so that we don't have to clean-up the files, the operating system will do that for us at some point
curl -f https://[...]/package-lock.json -o package-lock.json- get the
package-lock.jsonfile from the GitHub repository of Eleventy and save it topackage-lock.jsonfile in the current (temporary) directory
- get the
prefetch-npm-deps package-lock.json > $pkgs/11ty-eleventy.sha256- calculate the hash with of the
package-lock.jsonfile withprefetch-npm-deps - and save it (
>) to$pkgs/11ty-eleventy.sha256file in our repository
- calculate the hash with of the
cat package-lock.json | jq --raw-output ".version" > $pkgs/11ty-eleventy.version- print out the
package-lock.jsonfile (cat) and pass it along (|) tojqwhich extracts the version ('.version') and saves it (>) to$pkgs/11ty-eleventy.versionfile in our repository
- print out the
To get the new dependencies in your development shell, make sure you leave (CTRL+D) and re-enter the development shell (nix develop).
Now we can run just update-eleventy.
You'll see that two new files appeared in the pkgs folder, a 11ty-eleventy.sha256 and a 11ty-eleventy.version.
If you open them, you'll see what you expect to see from the filename, a hash and a version.
Let's use these two new files in our pkgs/11ty-eleventy.nix file.
I used a suffix of main for the version, since we're using that branch of the Eleventy repo.
A better approach would be to use a tagged release instead.
diff --git a/pkgs/11ty-eleventy.nix b/pkgs/11ty-eleventy.nix
index c6adbfa..240d557 100644
--- a/pkgs/11ty-eleventy.nix
+++ b/pkgs/11ty-eleventy.nix
@@ -3,9 +3,9 @@
pkgs.buildNpmPackage {
# Package name
pname = "eleventy";
- version = "1.0.0";
+ version = (builtins.readFile ./11ty-eleventy.version) + "main";
src = src;
- npmDepsHash = "sha256-LGdCM1gjt3hRn7BiIlbA4e2HOiQ6e/qkAtWp0Qwn+PE=";
+ npmDepsHash = builtins.readFile ./11ty-eleventy.sha256;
dontNpmBuild = true;
meta = with pkgs.lib; {
description = "A simpler static site generator";
And after adding both new files to our git (git add pkgs), we can try and build eleventy again with nix build .#eleventy.
Success!
Taking a look at the nix store, we can see it matches the version reported by eleventy.
❯ readlink result
/nix/store/j7kr145ws70in0sbj91yn2ba0m64kmih-eleventy-3.1.2-beta.2-main
❯ result/bin/eleventy --version
3.1.2-beta.2
One thing to note here is that if you only run just update-eleventy and don't run just update (nix flake update alias), the hash will at some point go out of sync, since flake.lock will be frozen in time and so will the src that we're passing into pkgs/11ty-eleventy.nix, but the package-lock.json that we're fetching will not be.
You'll get a hash mismatch and you'll have to update the flake.lock file with just update.
So let's fix that and make sure both are run at the same time.
diff --git a/justfile b/justfile
index c7450cb..8de6672 100644
--- a/justfile
+++ b/justfile
@@ -9,11 +9,11 @@ help:
# Update project dependencies
[group('General')]
-update:
+update: _update-eleventy
nix flake update
# Update packages sha256 and version
-update-eleventy:
+_update-eleventy:
#!/bin/bash
pkgs=$(pwd)/pkgs
cd $(mktemp -d)
So now whenever you run just update, it will also run _update-eleventy, thus they will never be out of sync.
I've also made the update-eleventy recipe a private one by prepending _ in front of it.
One last thing before we git commit, let's ignore the result in our .gitignore file (echo "result" > .gitignore).
Commit: Add @11ty/eleventy package to dev shell
Phew! That was quite some work we put in. Now let's build our static site.
Build with Eleventy
Creating an Eleventy generated static site is pretty straightforward, they really do live up to their slogan.
Let's create an index.html file in our repo root.
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
That should do it.
Now let's enter our development shell and run eleventy to build the static site.
(I also gitignored _site at this point echo "_site" >> .gitignore).
❯ eleventy
[11ty] Writing ./_site/index.html from ./index.html (liquid)
[11ty] Wrote 1 file in 0.04 seconds (v3.1.2-beta.2)
Looks good! Let's also try if live development works.
❯ eleventy --serve
[11ty] Writing ./_site/index.html from ./index.html (liquid)
[11ty] Wrote 1 file in 0.05 seconds (v3.1.2-beta.2)
[11ty] Watching…
[11ty] Server at http://localhost:8080/
[11ty] File changed: ./index.html
[11ty] Writing ./_site/index.html from ./index.html (liquid)
[11ty] Wrote 1 file in 0.01 seconds (v3.1.2-beta.2)
[11ty] Watching…
I edited the index.html and the page updated by itself, awesome!
I think we're done here. git commit
Commit: Add index.html
Now let's build the site, with Nix.
Build with Nix
You might be wondering why build with Nix, didn't we just build with Eleventy?
Can't I just take the files in _site, copy them to some hosting site and serve them?
Of course. That would be perfectly fine and I won't hold it against you if you want to stop here and do that. Congrats on your new static site!
But if you have NixOS as your server and you're like me and dislike manual work, then continue reading and I will show you how Nix wants you to do this. And I promise it will be easier this time!
Alrighty!
diff --git a/flake.nix b/flake.nix
index dd6cc6d..b31a478 100644
--- a/flake.nix
+++ b/flake.nix
@@ -24,9 +24,24 @@
inherit pkgs;
src = inputs.eleventy-src;
};
+
+ # The static site
+ site = pkgs.stdenv.mkDerivation {
+ pname = "my-static-site";
+ version = "1.0.0";
+ src = ./.;
+ buildInputs = [ eleventy ];
+ buildPhase = "eleventy";
+ installPhase = ''
+ mkdir -p $out/
+ cp -r _site/* $out/
+ '';
+ };
in
{
packages.eleventy = eleventy;
+ packages.site = site;
+ packages.default = site;
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
just
There is a couple of things going on here.
First, we created a new derivation/package by calling mkDerivation and assigned to a variable called site.
In Nix-land almost everything is a derivation, you get used to it.
We set a few things for this derivation:
pname- package name
- you'll probably set this to the name of your static site
version- a version number, if anyone uses your package, they might want to know about major version changes, etc
src- this is what the derivation will consider as its source files, in our case its the current directory (our repo root)
- any changes to the source files (even unrelated to the static site, like
.gitignore) will change the derivation
buildInputs- these are similar to the build inputs that we have for our development environment
- since in our build and install phases we only use
eleventyand sincemkdirandcpare already part of thestdenv, we only have to specifyeleventyhere
buildPhase- Nix builds the package in phases
- in this phase we should produce the built files somehow, in our case that is running the
eleventybinary and producing the_sitefolder
installPhase- in this phase Nix expects us to create the
$outfolder and copy the files that we want to be part of the package there $outvariable resolves to the Nix store path that the package will occupy- in our case, we want the files in
_siteto be part of the package, so we copy them
- in this phase Nix expects us to create the
And then we make the package available as an output of our flake. Let's take a look.
❯ nix flake show
[...]
git+file://[...]/create-a-static-blog-with-nix
├───devShells
│ ├───aarch64-darwin
│ │ └───default: development environment 'nix-shell'
[...]
└───packages
├───aarch64-darwin
│ ├───default: package 'my-static-site-1.0.0'
│ ├───eleventy: package 'eleventy-3.1.2-beta.2-main'
│ └───site: package 'my-static-site-1.0.0'
[...]
We've made it available under two different names site and default.
The default package is a bit special in the sense that you can build it simply with nix build.
Let's try it!
❯ nix build
❯ readlink result
/nix/store/25m04s0xmfz2cwlbj1nq7iw9fas9h3jy-my-static-site-1.0.0
❯ cat result/index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
[...]
And there you have it! You've built the site with Nix! That wasn't so bad, was it?
Let's git commit and continue on to deploying the site.
Commit: Build the site with nix
Deploy!
Now we can deploy it! Woo!
So what we'll do is:
- create a new NixOS configuration that can run as a VM
- in the VM, start a Caddy web server
- configure Caddy to serve our static site
- add a test user to the VM, so we can log in and test that everything works
In this section I will just present the end result, as there are many ways to achieve the same thing and a lot of it will depend on your existing/desired setup.
Before we proceed, we'll need to push the static site repository to a remote. Instructions on how to do this will again vary depending on the forge you're using. I use a self-hosted git forge (Forgejo), so I:
- created an empty repository there
git remote add origin git@git.kalu.blue:tech-blog/create-a-static-blog-with-nix.gitgit push -u origin main
For convenience I created a sub-directory nixos-configuration in our static site repository to contain the NixOS configuration, but you should create a new repository to contain the configuration for your NixOS server.
Here are the files that I added to that directory:
❯ tree nixos-configuration
nixos-configuration
├── flake.lock
├── flake.nix
├── justfile
└── my-static-site.nix
1 directory, 4 files
Here is the flake.nix. flake.lock will be created for you once you run some nix command (or if you want to mirror my example 100%, you can copy it from the post repository).
I created a separate module file for our static site, just to demonstrate how to pass the flake inputs to modules as an argument (see specialArgs below).
There is also one (inline) module that handles things needed to run this configuration as a VM.
Keep in mind the configuration as-is only works on x86_64-linux systems.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
our-site = {
url = "git+ssh://git@git.kalu.blue/tech-blog/create-a-static-blog-with-nix.git?ref=main&shallow=1";
inputs.nixpkgs.follows = "nixpkgs"; # Will use the same Nixpkgs as the NixOS system
};
};
outputs =
{ self, nixpkgs, ... }@inputs:
{
nixosConfigurations.my-server-name = nixpkgs.lib.nixosSystem {
specialArgs = {
# These will be passed as arguments to the modules
inherit inputs;
};
modules = [
# Essential for test VM
{
virtualisation.vmVariant = {
virtualisation = {
memorySize = 2048;
cores = 2;
# Disable graphics to avoid gtk error
graphics = false;
};
};
# Create a user, so we can login and test
users.users.test = {
isNormalUser = true;
# Never use this, use `hashedPassword` instead
initialPassword = nixpkgs.lib.mkForce "123123";
# So we can use `sudo` mainly for `sudo shutdown now` and to be able to debug `caddy`
group = "wheel";
};
}
# Some configuration
{
nixpkgs.hostPlatform = "x86_64-linux";
system.stateVersion = "25.11";
}
./my-static-site.nix
];
};
};
}
Then the module my-static-site.nix.
We have to enable the caddy service and configure one virtual host (example.com) with some configuration that will set it to serve files from our static site package.
We also add some gzip compression.
As a bonus, I also included a curl-site shell alias, which you can use in the VM to view our static site main page.
I think it also nicely demonstrates how simple it is to use one variable to configure two vastly different systems (web server and shell aliases).
{ inputs, pkgs, ... }:
let
site-url = "example.com";
in
{
services.caddy = {
enable = true;
# Needs `http://` prefix so that it does not try to request TLS certificates and redirect to 443
virtualHosts."http://${site-url}".extraConfig = ''
file_server
root * ${inputs.our-site.packages."${pkgs.system}".default}
encode gzip
'';
};
environment.shellAliases = {
# Test our site
curl-site = "curl -H \"Host: ${site-url}\" localhost";
};
}
This configuration will start Caddy and set it serve our static site.
It will only be available on localhost, since we have not opened any firewall ports (e.g. 80 and 443).
But it was enough for me to start the VM and verify that everything works as intended.
I've also added a just vm recipe (runs nix run ".#nixosConfigurations.my-server-name.config.system.build.vm").
I think its really neat how simple it is to start a full-blown virtual machine with Nix.
Anyway enter the nixos-configuration folder and run just vm.
It will build and start the virtual machine and after a while a login screen will be presented.
We have configured a user test with a plain-text password 123123.
Use that to log in.
Now we can inspect the system:
systemctl status caddy- we can see its running
- its using config located at
/etc/caddy/caddy_config
cat /etc/caddy/caddy_config- taking a peek at the config file reveals the root path that our web server is serving file from
cat /nix/store/cr5wkgbijfhhp0dy1qm2k30wx8j83bzh-my-static-site-1.0.0/index.html- yep, this is the
index.htmlthat we wrote
- yep, this is the
curl-site- yep, this is the
index.htmlfile we expected to be served from our web server
- yep, this is the
❯ cd nixos-configuration
❯ just vm
nix run ".#nixosConfigurations.my-server-name.config.system.build.vm"
[...]
<<< Welcome to NixOS 25.11.20250618.5395fb3 (x86_64) - ttyS0 >>>
Run 'nixos-help' for the NixOS manual.
nixos login: test
Password:
[test@nixos:~]$ systemctl status caddy
● caddy.service - Caddy
Loaded: loaded (/etc/systemd/system/caddy.service; enabled; preset: ignored)
Drop-In: /nix/store/bg47546cl8k10w06wg3i002frn2c422f-system-units/caddy.service.d
└─overrides.conf
Active: active (running) since Sun 2025-06-22 18:06:27 UTC; 1min 31s ago
Invocation: 82109b58487c4df48a9d15ba2a958d7e
Docs: https://caddyserver.com/docs/
Main PID: 873 (caddy)
IP: 0B in, 0B out
IO: 40K read, 8K written
Tasks: 8 (limit: 2343)
Memory: 32.6M (peak: 33M)
CPU: 992ms
CGroup: /system.slice/caddy.service
└─873 /nix/store/9820hkh6vc96hjmbqpi9xzfjdrgsw17i-caddy-2.10.0/bin/caddy run --config /etc/caddy/caddy_config --adapter ca>
Jun 22 18:06:25 nixos systemd[1]: Starting Caddy...
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5026293,"msg":"maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined"}
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5131006,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachin>
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5194187,"msg":"using config from file","file":"/etc/caddy/caddy_confi>
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5768142,"msg":"adapted config to JSON","adapter":"caddyfile"}
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.6890154,"msg":"serving initial configuration"}
Jun 22 18:06:27 nixos systemd[1]: Started Caddy.
[test@nixos:~]$ cat /etc/caddy/caddy_config
{
log {
level ERROR
}
}
http://example.com {
log {
output file /var/log/caddy/access-http:__example.com.log
}
file_server
root * /nix/store/cr5wkgbijfhhp0dy1qm2k30wx8j83bzh-my-static-site-1.0.0
encode gzip
}
[test@nixos:~]$ cat /nix/store/cr5wkgbijfhhp0dy1qm2k30wx8j83bzh-my-static-site-1.0.0/index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>Eleventy rocks</p>
</body>
</html>
[test@nixos:~]$ curl-site
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>Eleventy rocks</p>
</body>
</html>
[test@nixos:~] sudo shutdown now
Everything is working as expected. Hooray!
Commit: Add an example nixos config using this site
Conclusion
We've successfully created a fully reproducible static blog setup using Nix and Eleventy. What started as avoiding a simple Node.js installation turned into a powerful development workflow that demonstrates the strength of Nix's approach to package management and deployment.
While this approach required more upfront investment than a traditional Node.js setup, we now have a system that "just works" across different machines and time. No more "works on my machine" problems, no dependency conflicts, and deployments are as simple as pointing to a git commit.
Next steps
Here are a few things you might want to do next:
- As you expand the site, you should move the static site source files to the folder
src. - Instead of using the main branch of Eleventy, you should switch to a tagged release. For me, living on the edge feels fine, for you it might not.
- You'll probably want to expose the site to the world wide web.
That would mean at the very least opening the ports
80and443, setting up DNS records to point to the machine and getting a valid TLS certificate (remove thehttp://prefix from the module). If you're running the machine at home, you might also need to do some port forwarding.