Gitea/Forgejo action runners, the host, and static sites

Hey folks, been on a bit of a journey to get a static site built on Forgejo hosted on NixOS and it’s proving more challenging than I realize. Hoping someone can help.

The setup: I have a Zola static site whose code is in a Forgejo container on a larger NixOS server. That server also directly hosts (I.e. not in a container) a Forgejo action runner with both ubuntu-latest and selfhosted labels so it can run actions both in containers and on the native host. It also runs caddy, does some reverse-proxying, and serves up a few existing static files for various things that were too trivial to containerize. The goal is to run complex actions in containers as you normally would, but also to have limited touchpoints with the runner host. In this case, /var/www/example.com is chowned to the Forgejo runner UID/GID, and I’m using a workflow like this to build the site:

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v4
        with:
          submodules: true
      - name: Install Zola
        uses: https://github.com/taiki-e/install-action@v2
        with:
          tool: zola@0.19.1
      - name: Build
        run: zola build
      - name: Save artifact
        uses: actions/upload-artifact@v3
        with:
          name: website
          path: public
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

  deploy:
    runs-on: native
    needs: build
    steps:
      - name: Restore artifact
        uses: actions/download-artifact@v3
        with:
          name: website
      - name: deploy
        run: rsync -a ./ /var/www/example.com

Here the first job builds the Zola site in a container, and the second copies the artifact data into /var/www/example.com via rsync.

Unfortunately this doesn’t work because I get a “read-only filesystem” error, and the rsync appears to attempt to create the directory that I already have, suggesting a chroot. I’m guessing, based on the existence of the services.gitea-actions-runner.instances.<name>.hostPackages option to which I had to add rsync, that the entire native/self-hosted job is running in a very restricted shell? If that’s the case:

  1. Is there a way to make /var/www/example.com from the host, or /var/www, available to the runner action?
  2. If not, or even if so, is there a better way to do this? I was just hoping to dump static files to be served up by the same caddy that’s doing a bunch of reverse-proxying to containers, but I’m starting to worry that I’ll have to package everything in a OCI container and run it all that way. Really hoping to avoid that if I can.

Thanks.

I think I see the cause but not the fix. When building the systemd unit for the runner, the module sets DynamicUser = true which locks down a bunch of filesystem access.

This is fine, but I’d like to inject an override of some sort maybe via ReadWritePaths, but I’m not good enough at Nix to figure out how. Given this definition for the runner, is it possible to modify the generated unit somehow even though the module itself doesn’t expose that setting?

    gitea-actions-runner = {
      package = pkgs.unstable.forgejo-runner;
      instances."${name}-linux" = {
        name = "Linux";
        enable = true;
        url = "https://${domain}";
        labels = [
          "ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04"
          "native:host"
        ];
        settings = {
          container = {
            network = "bridge";
          };
        };
        hostPackages = with pkgs; [
          bash
          coreutils
          curl
          gawk
          gitMinimal
          gnused
          nodejs
          rsync
          wget
        ];
        tokenFile = config.age.secrets."${name}_runner_linux".path;
      };
    };

Thanks.

You can add your destination path to the ReadWritePaths of the generated unit like so:

systemd.services."gitea-runner-${name}-linux".serviceConfig.ReadWritePaths="/var/www/example.com";

Then it should work.

Thanks! I ended up going with this which seems to work:

  systemd.services."gitea-runner-${utils.escapeSystemdPath "${name}-linux"}".serviceConfig.ReadWritePaths =
    "/var/www";