My penguin avatar

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:

In YAML it looks like this:

    build: ./src/forgejo-runner
    command: "daemon"
      - /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:

    build: ./src/forgejo-runner
    command: "daemon --config config.yml"
      - /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 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:

  enabled: true
  host: forgejo-runner
  port: 8080
  network: docker_default

Additionally, expose port 8080 on the container:

      - "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:

Lastly, enable Actions in the repository settings, push some changes, and hope for the best :^)

Loading posts...