Files
homepage/content/posts/better-image-building.md
T
2024-04-30 23:17:02 -05:00

230 lines
8.6 KiB
Markdown

---
title: "Better Image Building"
date: 2024-04-30T22:18:35-05:00
draft: false
tags:
- Nix
- NixOS
- Flakes
- CI
---
I've managed to recently upgrade my image building process for this
website, using some of what I have been learning about
[Nix](https://nixos.org/). For a while
while I was first learning to use Nix I thoguht of the language, the
packages, and the OS as being one thing. And this did a disservice to
both the breadth of what Nix offers as well as the capabilities it
offers.
## Old Paradigm
So previously with this site I used a fairly straightforward and manual
process. I installed [Hugo](https://gohugo.io/) on my local machine with
dnf, ran the dev server, and
edited my pages. When I pushed the commits into GitHub, I had a
`Containerfile` in the repository that would get built with Docker or
[Podman](https://podman.io/), pushing the resulting image to my image
repository. I used a
multi-stage file, first pulling the Hugo image to build the files and
then grabbing the resulting build produts to place them into the
[Nginx](https://nginx.org/en/) image.
When I first learned how to use NixOS I spun up [Gitea](https://nginx.org/en/),
added a [flake.nix](https://src.thehellings.com/greg/homepage/-/blob/main/flake.nix)
file that would offer me Hugo locally through direnv, and wrote a shell
script that would test building the Containerfile locally to give myself
a sanity test. I felt that this was a pretty snappy way to handle it.
Now, anywhere I had Nix, I could just check out my code and run the
development bits of it.
## More Nix
Although I have since moved my self-hosting to Gitlab instead of Gitea,
I was operating under the same basic flow. However, recently, I beefed up
my use of Nix in a few nifty ways that stray far from just leveraging the
pacakages and the OS. Now, the whole cycle of the build process is
managed by Nix.
In my flake.nix file, I have added a few enhancements. First, I added
[devshell](https://github.com/numtide/devshell). This is not to be
confused with the built-in devShells from flakes themselves. While there
is nothing in devshell that you cannot do with the built-in tool, there
is just so much boiler-plate built in. For one, it gives me a nice
listing of all the tools I want to highlight in this development.
Secondly, it produces a very nice motd by default to help alert the
developer that they are entering the shell (this is helpful for me,
as I use direnv to automate activating my shell environments).
The second enhancement I added was using a flake to wrap
[process-compose](https://community.flake.parts/process-compose-flake).
This eliminates the need for me to write a shell script to start up
Hugo or remember the exact command to use. Plus, process-compose has
a very nice Curses tui to show the status of the applications you
are running. While starting up the hugo server is not exactly
rocket science, process-compose is capable of far more than just
starting one service in a tui. You can start up databases, caching
services, and more with a single command and see them all nicely
displayed in the tui. The available options are quite extensive.
While they might be slight overkill for just this site, the power
that can be shown for more complex development work is impressive.
Spinning up whole fleets of VMs or other servies, allowing for
dependencies and readiness checks. The ability to spin up a reproducible
environment for development work with such a tool is very impressive.
## Image Building with Nix
I was reading someone else's blog post a few weeks ago about building
OCI container images with Nix and how the images you build are even
better than the ones you can build with a Dockerfile or Containerfile.
I had my doubts, at first, but as I looked into the subject, it turns
out this is actually pretty true. On top of the reproducible ability
for packages that are identified with a `flake.lock` file which
you do not get when using a different base for your container,
a layered image from Nix creates one layer per package you add to
the image. This means that every image I build and deploy with
my Nix tools can share the same set of base packages. Yes, I hear
you say, if you build a dozen different images from the same
base image, all of those resulting images will share that base
layer. And that is true. But when you start to install other
packages on top of it, unless you install all of the pacakges
in the same way and the same order in every separate Containerfile,
you will not have the benefit of sharing the layers above that
base image. With a Nix built container, this part is already handled
by the tooling directly.
So how do I build this container?
### Build the site
First, I create a derivation that builds and packages my site's static
version:
```nix
site = pkgs.stdenv.mkDerivation {
name = "Greg's homepage";
nativeBuildInputs = [ pkgs.hugo ];
buildPhase = ''
hugo -v
'';
installPhase = ''
mkdir -p $out
cp -R public/* public/.* $out
'';
src = ./.;
};
```
This derivation is simple. It uses `pkgs.hugo` as a native build
input, executes hugo against the repository, and copies the
resulting files to the `$out` target. It isn't important now, but
I build this as a package in the flake outputs. I could easily
have put it as just local variable, or such. But this example
shows easily how to build the local package directly in the
included flake.
### Create Nginx Config
I still plan to create a static site that is hosted by Nginx
in my container. To accomplish this, Nginx will need a very
minimal configuration. At least for now. So I add another
package to the flake outputs that just creates the Nginx file:
```nix
nginx-config = pkgs.writeText "nginx.conf" ''
user root root;
worker_processes 10;
pid /tmp/nginx.pid;
daemon off;
error_log stderr;
events {
}
http {
access_log stdout;
include ${pkgs.nginx}/conf/mime.types;
server {
listen 80;
root ${self'.packages.site};
autoindex off;
}
}
'';
```
This derivation is even simpler than the previous one. I
leverage the `pkgs.writeText` function to create the
configuration file for Nginx. Unlike in the official version
of the Nginx container, there is no need here to copy my
resulting site files to some path. I can directly pass the
path of my site to this package. You can see this happen
at the line `root ${self'.packages.site}`. In this case, the
value of `self'` is just what I use to reference the flake
itself. Since `site` is the derivation that includes the built
site products, I can just reference this directly.
### Create the Image
Again, this is just a static site, so the resulting image is
fairly mundane and straightforward. However, it is telling
for how easy it is to build up the resulting packages and
derivations into a simple image. I choose to make the container
image the default package for this particular flake repo. And
I am sure to build a layered image so that I can make the most
of caching and reusing shared packages with any other containers
I might run. Here is how I build the image:
```nix
default = pkgs.dockerTools.buildLayeredImage {
name = "gregs-homepage";
tag = "latest";
contents = [
self'.packages.site
(pkgs.buildEnv {
name = "image";
paths = with pkgs; [
dockerTools.fakeNss
nginx
];
pathsToLink = [ "/bin" "/etc" "/var" ];
})
];
fakeRootCommands = ''
mkdir tmp
chmod 0777 tmp
'';
config = {
Cmd = [ (lib.getExe pkgs.nginx) "-c" self'.packages.nginx-config "-e" "stderr" ];
Port = [ 80 ];
};
};
```
Here we can see a simple image with Nginx to serve up the
base image that is needed. Now, with this added into the flake
package output, I can run `nix build .` and Nix will grab the
exact version of Hugo that I use, build my site, and package
it into an OCI container image. By default, nix build drops
a symlink to the final derivation in the current directory
with the name `result`. Using Podman or Docker, I can import
my image with `podman load -i result` and my image `gregs-homepage:latest`
will be loaded into my local images. I can then tag and push
it anywhere I want.
## Conclusion
I know there is nothing Earth shattering here. There is not
anything that cannot be done manually or with some amount of
effort using other tools. What is so nice about Nix is that
it wraps up all that manual and boiler plate bits that I
would probably never have spent the time to write all for myself
and gives it to me in very nice, neat packages.
What do you use to simplify your development, building, and
deployment process? So far, Nix is great for development
and building, and I use NixOS on all my servers. But I'm
still hoping to get even deeper into the Nix world and find
or create some good deployment tooling.