Creating a base OCI image for Nix flake builds within Gitea/Forgejo


nix  nixos  ci  docker  oci 

I’ve been moving more and more of my infrastructure to be self-hosted recently. Part of that involves setting up CI jobs for testing and publishing artifacts, mostly rust crates but also this very blog.

I really wanted to re-use my existing Nix flakes for those projects, this way I know my local dev env would be the same env then used on CI.

I am self-hosting a Gitea instance (will probably be migrating to Forgejo) and it uses a CI system built to resemble Github actions - basically you run your jobs as containers and within those you can run arbitrary commands. You can also take advantage of the existing ecosystem of actions.

I wanted a base image that would have on one hand nix with flakes enabled but on the other hand would be compatible with running popular actions from other authors. This meant having nix, git but also nodejs available amongts other things. I couldn’t find one that would have both, so I built one !

I’m building on top of the definitions from docker-nixpkgs and just tweaking them to add the things needed for actions and also for the definition itself to be a flake, for an added flavour. This allows me to add multiple image definitions in the same repo and the build them independently when needed.

Here’s the whole definition in all of its glory, defining 2 images - hello and flakes-action - the hello being a test image for testing the process itself and flakes-action is the one I’m using on CI currently.

{
  description = "docker base images";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem
      (system:
        let
          pkgs = import nixpkgs { inherit system; };
          pkgsStatic = pkgs.pkgsStatic;
          lib = pkgs.lib;

        in
        {
          packages = {
            hello = pkgs.dockerTools.buildImage {
              name = "hello-docker";
              config = {
                Cmd = [ "${pkgs.hello}/bin/hello" ];
              };
            };
            flakes-action = pkgs.dockerTools.buildImageWithNixDb {
              name = "flakes-action";
              contents = with pkgs; [
                ./root
                bash
                coreutils
                curl
                gawk
                gitFull
                git-lfs
                gnused
                nodejs
                wget
                sudo
                nixFlakes
                cacert
                gnutar
                gzip
                openssh
                xz
                (pkgs.writeTextFile {
                  name = "nix.conf";
                  destination = "/etc/nix/nix.conf";
                  text = ''
                    accept-flake-config = true
                    experimental-features = nix-command flakes
                  '';
                })
              ];

              extraCommands = ''
                # for /usr/bin/env
                mkdir usr
                ln -s ../bin usr/bin

                # make sure /tmp exists
                mkdir -m 1777 tmp

                # need a HOME
                mkdir -vp root
              '';
              config = {
                Cmd = [ "/bin/bash" ];
                Env = [
                  "LANG=en_GB.UTF-8"
                  "ENV=/etc/profile.d/nix.sh"
                  "BASH_ENV=/etc/profile.d/nix.sh"
                  "NIX_BUILD_SHELL=/bin/bash"
                  "NIX_PATH=nixpkgs=${./fake_nixpkgs}"
                  "PAGER=cat"
                  "PATH=/usr/bin:/bin"
                  "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
                  "USER=root"
                ];
              };
            };
          };
        });
}

If you want to build this yourself you can:

git clone https://git.cyplo.dev/cyplo/base-images.git
cd base-images
nix build '.#flakes-action'
docker load < result # this took me so much time, to realise I need `load` and not `import`...
docker tag [image id] yourimage.repo/base-images/flakes-action:latest
docker push yourimage.repo/base-images/flakes-action:latest

Then to use on CI, an example of a Gitea CI config:

on: push
jobs:
  Publish:
    runs-on: flakes-action
    steps:
      - uses: actions/checkout@v3
        name: Checkout
      - name: Build 
        run: |
          nix develop -c hugo --gc --minify          

It uses the image pushed and both a custom build script but also a well-known checkout action.

You need to teach your Gitea runner about the image first btw; if you use NixOS for the runner definition, it could look like this:

services.gitea-actions-runner = {
  instances.boltyone = {
    enable = true;
    url = "https://yourgitea.domain";
    tokenFile = config.sops.secrets."gitea-runner-token".path;
    name = "bolty one";
    labels = [
      "flakes-action:docker://yourimage.repo/base-images/flakes-action:latest"
      "ubuntu-kinetic:docker://ubuntu:kinetic"
      "linux_amd64:host"
    ];
  };
};

P.S. shoutout to nixery that I tried first and the resulting images were just a bit off as it was not easy to get them to support flakes. I think it’s an amazing tool in its own right though and you should try it, you can do things like docker run -ti nixery.dev/shell/git/htop bash and it will happily just give you an image with those arbitrary nixpkgs included !

Happy hacking !

Discuss this post on the Fediverse

nix  nixos  ci  docker  oci 

See also