🌐 Building Jekyll website with Nix
For a long time, I have been building my Jekyll website on CI using Docker. Plain Ruby image, installing dependencies, caching, running Jekyll build. On local machine I used to manage Ruby development environment. This is a working solution, and generally there is nothing wrong with it. However, right now, I am continuously migrating everything to Nix, and this website is no exception. My environment is mostly managed by Nix. I also had a great experience using it to build a real project, and now it’s time to move to Nix to build this website. In addition to all the benefits of Nix, it’s also good thing to get rid of another “out-of-store” dependency like Docker 👺.
Next I assume that you already have Nix installed.
Alternative configurations
There are some articles about building Jekyll using Nix, such as:
- Build a Jekyll blog with Nix using flakes
- Using Jekyll and Nix to blog
- Building a Jekyll Environment with NixOS
However, all of them are a little bit outdated. Still, there is some interesting information to read, but nowadays there is much more easier way to configure Jekyll + Nix, so read on.
How it will look like
After setting things up, you can use the following commands to interact with your Jekyll website (serve, build and so on):
# defaults to `jekyll build`
nix run .
# `nix run . args` expands to `jekyll args`
nix run . serve
# `nix run .#bundle args` expands to `bundle args`
nix run .#bundle lock
# `nix run .#bundix -- args` expands to `bundix args`
nix run .#bundix -- -m
# Enters development shell with all dependencies installed
nix develop
Sounds nasty: no Dockerfile
configuration, no global dependencies, use the single tool to develop locally and build on CI.
So, let’s check the necessary Nix configurations which you need to fill in.
Jekyll + Nix configuration
Everything you need is a single file flake.nix
which looks like that:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
bundix = {
url = "github:inscapist/bundix/main";
inputs.nixpkgs.follows = "nixpkgs";
};
ruby-nix = {
url = "github:inscapist/ruby-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
bundix,
ruby-nix
}: let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ ruby-nix.overlays.ruby ];
};
rubyNix = ruby-nix.lib pkgs;
bundixcli = bundix.packages.${system}.default;
deps = with pkgs; [ env ruby bundixcli ];
inherit (rubyNix {
name = "seroperson.gitlab.io";
gemset = ./gemset.nix;
gemConfig = pkgs.defaultGemConfig;
})
env ruby;
in {
packages.${system} = let
bundlecli = pkgs.writeShellApplication {
name = "bundle";
runtimeInputs = deps;
text = ''
export BUNDLE_PATH=vendor/bundle
bundle "$@"
'';
};
jekyll = pkgs.writeShellApplication {
name = "jekyll";
runtimeInputs = deps;
text = ''
if [ $# -eq 0 ]; then
jekyll build
else
jekyll "$@"
fi
'';
};
in {
jekyll = jekyll;
bundle = bundlecli;
bundix = bundixcli;
default = jekyll;
};
devShells.${system}.default = pkgs.mkShell {
shellHook = ''
export BUNDLE_PATH=vendor/bundle
'';
buildInputs = deps;
};
};
}
In short, it’s just the default ruby-nix configuration and some Nix aliases. Nothing more, actually. In fact, the minimal Nix configuration for Jekyll is even shorter, but those aliases are so useful that I decided to include them here too.
When flake.nix
is created, you will need to generate a gemset.nix
file by running the following command:
nix run .#bundix -- -m
bundix
is a tool which pins dependencies for Nix according to Gemfile.lock
. Be sure to put gemset.nix
under a git control and further remember to keep your Gemfile.lock
and gemset.nix
files synchronized.
Finally, don’t forget to edit your Jekyll _config.yml
to not publish your nix files:
# ...
exclude:
- gemset.nix
- flake.nix
- flake.lock
# ...
That’s it. Now everything should work.
Additional configuration for plugins with native libraries
If you are using the jekyll_picture_tag (with libvips
) or any other native library, you may also need to add some additional configuration to make everything work. Many libraries are already covered by Nix (with a bunch of hacks), and mostly they just work, but sometimes strange things happen. If errors like Could not open library
occur, this is probably your case. The solution differs for each case, but the general approach is the same: in the package sources, you should replace the default native library references with nixified references. That’s how it looks like for ruby-vips
:
{
inputs = {
# ...
};
outputs = {
# ...
}: let
# ...
inherit (rubyNix {
# ...
gemConfig = pkgs.defaultGemConfig // {
ruby-vips = attrs: {
postInstall = ''
cd "$(cat $out/nix-support/gem-meta/install-path)"
substituteInPlace lib/vips.rb \
--replace "library_name('vips', 42)" '"${pkgs.vips.out}/lib/libvips${pkgs.stdenv.hostPlatform.extensions.sharedLibrary}"' \
--replace "library_name('glib-2.0', 0)" '"${pkgs.glib.out}/lib/libglib-2.0${pkgs.stdenv.hostPlatform.extensions.sharedLibrary}"' \
--replace "library_name('gobject-2.0', 0)" '"${pkgs.glib.out}/lib/libgobject-2.0${pkgs.stdenv.hostPlatform.extensions.sharedLibrary}"'
'';
};
};
})
env ruby;
in {
# ...
};
}
Configuring CI with nixified Jekyll
Well, now we are building our Jekyll website using Nix. We are building it locally, so why not build it in the same way on CI? This allows us to use the same environment on both CI and the local machine, eliminating redundant CI-only configuration and simplifying the pipeline. That’s how it looks like for GitLab CI using cynerd/gitlab-ci-nix:
.nix:
image: registry.gitlab.com/cynerd/gitlab-ci-nix
cache:
key: "nix"
paths:
- ".nix-cache"
before_script:
- gitlab-ci-nix-cache-before
after_script:
- gitlab-ci-nix-cache-after
pages:
extends: .nix
stage: deploy
script:
- nix run . -- build -d public && gzip -k -6 $(find public -type f)
artifacts:
paths:
- public
I’m sure there is an analogue image for GitHub CI, but honestly, I haven’t tested any.
Conclusion
Thank you for reading this little article, I hope it will be useful to someone. Feel free to reach me if you have something to say.
And also take a look on the posts on similar topics: