Build nix flakes in a gitea runner in NixOS
In this article, I'll show you how I set up my personal CI using gitea, nixos, and docker. I build the docker nix image directly from nix, and push it in gitea.
In my current set up, I have a server running gitea:
{ config, pkgs, ... }: {
systemd.tmpfiles.rules = [
"d /data/gitea 700 gitea gitea"
"d /data/gitea-backup 700 gitea gitea"
];
services.gitea = {
enable = true;
stateDir = "/data/gitea";
dump.enable = true;
dump.backupDir = "/data/gitea-backup";
settings.repository.ENABLE_PUSH_CREATE_USER = true; # this is awesome! you don't have to manually create your repository before pushing to it.
# <and a lot more unimportant configs>
};
}
And some runners in it:
{ token, runnerName }: { config, pkgs, ... }:
let
gitea-runner-directory = "/var/lib/private/gitea-runner";
gitea-runner = {
enable = true;
token = "${token}";
url = "<gitea url>";
labels = [
"gitea-latest:docker://<gitea-url>/<user>/gitea-base-runner:latest"
"debian-latest:docker://node:18-bullseye"
"ubuntu-latest:docker://ubuntu:latest"
"native:host"
"nix:docker://nixos/nix:latest"
];
settings = {
cache = {
enabled = true;
};
container = {
workdir_parent = "${gitea-runner-directory}/workspace";
force_pull = true;
};
host = {
workdir_parent = "${gitea-runner-directory}/action-cache-dir";
};
};
};
in
{
virtualisation.docker.enable = true;
services.gitea-actions-runner = {
instances = {
"${runnerName}" = gitea-runner // {
name = "${runnerName}";
};
};
};
# I happen to need that because it loads a lot of different docker networks
systemd.services."clean-docker-networks" = {
script = ''
set -eu
${pkgs.docker}/bin/docker network prune -f
'';
serviceConfig = {
Type = "oneshot";
User = "root";
};
};
systemd.timers."clean-docker-networks" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
Unit = "clean-docker-networks.service";
};
};
}
The runner image is pushed in the gitea registry, and fetched on each git push using a simple gitea action every time:
./.gitea/workflows
name: Run nix build on repo.
run-name: ${{ gitea.actor }} builds all.
on: [push]
jobs:
Build:
runs-on: gitea-latest
container:
credentials:
username: <redacted>
password: "${{ secrets.<redacted> }}"
steps:
- uses: actions/checkout@v4
- run: nix build
- run: nix store sign --all --key-file /root/nix-store-key
- run: nix copy --to 's3://<nix-cache-name>?profile=default&scheme=https&endpoint=example.com®ion=<region>' $(nix-store --query $(nix path-info --recursive --derivation))
I use this same file every time in my repos so that it pushes the derivations to my personal S3 (probably an article for later).
So using that file, it needs to be ran on a nixos machine. This is how I defined the Docker image, using nix, to build a nix image:
# ./flake.nix
# use official nix container as base to get everything for flake-based building
# https://github.com/NixOS/nix/blob/master/docker.nix
# https://discourse.nixos.org/t/how-to-build-a-docker-image-with-a-working-nix-inside-it/32960/3
# https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-dockerTools
{
inputs =
{
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
nix = {
url = "github:NixOS/nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nix, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
flakeRegistry = {
nixos = {
from = { type = "indirect"; id = "nixos"; };
to = nixpkgs;
exact = true;
};
nixpkgs = {
from = { type = "indirect"; id = "nixpkgs"; };
to = nixpkgs;
exact = true;
};
};
flakeRegistryJSON = (pkgs.formats.json { }).generate "flake-registry.json" {
version = 2;
flakes =
pkgs.lib.mapAttrsToList (n: v: { inherit (v) from to exact; }) flakeRegistry;
};
containerInfo = {
name = "custom-nixos";
tag = "latest";
};
container-layered = pkgs.lib.throwIfNot (pkgs.stdenv.isLinux) "Docker images are only supported on Linux." (import ("${nix.outPath}" + "/docker.nix") {
inherit (containerInfo) name tag;
inherit pkgs;
bundleNixpkgs = false;
extraPkgs = [ pkgs.git pkgs.which pkgs.stdenvNoCC pkgs.nodejs_20 pkgs.gnused ];
maxLayers = 90;
nixConf = {
experimental-features = [ "nix-command" "flakes" ];
access-tokens = "<github access token>";
extra-substituters = "<S3 URL>";
extra-trusted-substituters = "<s3 URL>";
extra-trusted-public-keys = "<nix-cache-name>:<S3 public key>";
};
flake-registry = flakeRegistryJSON;
});
final-container = pkgs.dockerTools.buildLayeredImage
{
fromImage = container-layered;
name = "<gitea-url>/luca/gitea-base-runner";
tag = "latest";
contents = [
./secrets
];
maxLayers = 100;
config = { };
fakeRootCommands = ''
set -e
mkdir -p /root/.aws/
mv /nix-store-key /root/
mv /credentials /root/.aws/credentials
'';
enableFakechroot = true;
};
in
rec {
packages = flake-utils.lib.flattenTree {
default = final-container;
};
}
);
}