Forgejo instances like Codeberg allow you to host your own Continuous Integration server.

Installation With Podman

Forgejo provides an OCI image, but it requires some setup. This assumes you have rootless podman already set up and maybe also a dedicated service account for this, that you have an open shell as.

  1. Create a directory on the host for the runner to store its data. This includes its configuration file.
  2. Launch an instance of the runner image with a terminal and data directory mounted. Note that the working directory within the container is /data, the directory being mounted.
podman run --rm -it -v <HOST DIR FOR DATA>:/data code.forgejo.org/forgejo/runner:<VERSION> bash
  1. From within the container, generate the default configuration.
forgejo-runner generate-config > config.yml
  1. For the cache server to work, containers spun up by the CI need to be able to reach the main container and so some config changes need to be made. First, create a new podman network as the default one does not support name resolution.
podman network create <NETWORK NAME>

There is a known bug in Ubuntu 22.04.1 LTS (report) that makes containers unable to use this network. (Workaround)

cache:
  host: "<CONTAINER NAME>"
  port: <PORT TO LISTEN ON>

container:
  network: "<NETWORK NAME>"
  1. Register the runner. This requires a token from your Forgejo instance.
forgejo-runner register --no-interactive --token <TOKEN> --name <DISPLAY NAME FOR INSTANCE> --instance <FORGEJO INSTANCE URL>
  1. The data directory on the host should now contain config.yml (which may be edited) and .runner (which may not)
  2. Leave the container with exit
  3. In order to spawn new containers for running jobs, the runner needs access to a podman socket. If the user account does not already have one set up, run systemctl --user enable podman.socket If you get an error like Failed to connect to bus: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined (which can happen if you've created a dedicated system account for this and used sudo to open a shell) then run the following and retry: export XDG_RUNTIME_DIR=/run/user/"$(id -u)"
  4. We're now ready to launch the container in daemon mode.
podman run \
-u root \
-d \
--name forgejo-runner \
--network <NETWORK NAME> \
-p <PORT TO LISTEN ON>:<PORT TO LISTEN ON> \
-v <HOST DIR FOR DATA>:/data \
-v ${XDG_RUNTIME_DIR}/podman/podman.sock:/var/run/docker.sock \
code.forgejo.org/forgejo/runner:<VERSION> \
forgejo-runner --config config.yml daemon
  1. Finally, we set up systemd to restart this container on boot.
podman generate systemd --restart-policy always --name forgejo-runner > ~/.config/systemd/user/container-forgejo-runner.service
systemctl --user daemon-reload
systemctl --user enable --now container-forgejo-runner

A couple of notes:

Testing Actions

The nice thing about having your own runner instance is that you can avoid polluting your commit history with the cycle of "change CI config, push, watch it fail, change CI config".

From within the container, the runner can be invoked to run a job directly from the CI config.

forgejo-runner exec --workflows <YML FILE HERE> --config /data/config.yml

Edits can be made to the CI config until it works, and then it can be committed and pushed afterwards.

Note: If the workflow includes a cache action, for some reason? this won't respect the cache server settings in the config.yml, then it'll try to reach a random port and wait until finally timing out.