Setting up a Self-Hosted Forgejo Actions Runner with Docker Compose
Published on 2023-07-07.I created an account on Codeberg the other day with the intention of hosting some of my code there in the future. So far, 100% of my open source activity happens on GitHub, and while they do a lot of things well, this seems like a good time to try alternatives considering the ongoing enshittification of major parts of the web.
I've been aware of Codeberg for a while, and after a friend (hi n0toose!) confirmed that the underlying software (Forgejo, a fork of Gitea) supports disabling pull request for a repository — a feature famously missing on GitHub — I was fully on board.
They optionally provide a hosted instance of Woodpecker CI for projects hosted on Codeberg, but I quickly ran into issues with workflows being slow and more importantly logs not loading, making the conversion from GitHub Actions a rather painful task. The other option is using Forgejo Actions, a relatively new feature building on top of act
for compatibility with existing GitHub Actions workflows. However, Codeberg currently doesn't provide hosted runners for this type of CI, so I looked into hosting my own :^)
Setting up a Forgejo Runner
Installation of the Forgejo Runner is covered in their documentation and doesn't need me writing this post. They only describe running directly on the host and in LXC containers; but since my entire self-hosted software stack runs on Docker Compose this wasn't an option.
Adding the Docker Compose service
Luckily a Dockerfile
is available, so I only had to figure out the configuration. Summarized we need the following:
- I haven't found a pre-built variant of the docker image anywhere, so the first step is cloning the repo (to
src/forgejo-runner/
in this case) and adding a service with abuild
entry - The
ENTRYPOINT
is a plain/bin/forgejo-runner
, so we add thedaemon
argument - A volume mounting
/var/run/docker.sock
from the host into the container so containers can be spawned for running workflows
In YAML it looks like this:
services:
forgejo-runner:
build: ./src/forgejo-runner
command: "daemon"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
At this point, we can register the runner with a Forgejo instance, in my case Codeberg. Follow the steps outlined here to obtain a registration token, then run:
docker compose run --rm --entrypoint sh forgejo-runner
…which will drop you into a shell within the container, from where we can invoke the registration:
forgejo-runner register
After completing all prompts, a file .runner
should be created, the contents of which need to be copied to the host so we can mount it as a volume later.
Configuring the Forgejo Runner
Additionally, create a config file for the runner (config.yml
) which will also be mounted as a volume and specified via a command line argument.
All available config options are explained in the docs, and if you're happy with the defaults you can skip this step. However, I'd definitely recommend changing runner.capacity
to something greater than one, and if you want to keep caching enabled we'll need to tweak a few other things. Updated Docker Compose config:
services:
forgejo-runner:
build: ./src/forgejo-runner
command: "daemon --config config.yml"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./volume/.runner:/.runner
- ./volume/config.yml:/config.yml
While I'm using bind mounts here, you can of course use any other available method of putting files into the container!
Making Caching Work
One of the hardest problems in computer science, and no exception here. Caching (e.g. for various setup actions and actions/cache
) is enabled by default, but the cache server will not be reachable from within the created workflow containers without intervention. This is because a random port is used by default (cache.port
set to 0
), which is not exposed on the runner container. I also had to tweak the container.network
option to use Docker Compose's docker_default
network which the runner container is reachable on.
Change this accordingly if your Docker networking setup is more involved :^)
This is what the config.yaml
file looks like for the above:
cache:
enabled: true
host: forgejo-runner
port: 8080
container:
network: docker_default
Additionally, expose port 8080 on the container:
services:
forgejo-runner:
...
expose:
- "8080"
Optionally, you can change the cache directory (/root/.cache
) and/or create a separate volume for it.
Putting It All together
Starting the container should now allow the daemon to run and show up as active on the Forgejo interface:
For me, importing existing workflow files written for GitHub Actions worked seamlessly after making the following changes:
- Moving them to
.forgejo/workflows/
(optional but less confusing than keeping.github/workflows/
IMO) - Setting
jobs.<job_id>.runs-on
todocker
(Labels in the screenshot above, not Name!) - Setting
jobs.<job_id>.container.image
toghcr.io/catthehacker/ubuntu:act-22.04
. Other images will work as well but might require additional setup steps, this one matches GitHub Actions closely. - Updating
jobs.<job_id>.steps[*].uses
URLs, e.g.uses: goto-bus-stop/setup-zig@v2
becomesuses: https://github.com/goto-bus-stop/setup-zig@v2
. This is not needed for actions mirrored on Codeberg, e.g.actions/checkout
(https://codeberg.org/actions/checkout
).
Lastly, enable Actions in the repository settings, push some changes, and hope for the best :^)