Domain Driven Architecture

HowTo: Setup forgejo runner and create basic actions

Autor: E.Seiert
July 30, 2025

Tags: package, runner, forgejo, actions

The forgejo actions feel very familliar to the CI of a well known git service. They are complete with env vars, secret handling, packages artifacts and releases. You can even self-host the runners and connect them to your forgejo instance (or even your account on codeberg.org). This HowTo will explain the setup of the runner in a k3s environment with an existing forgejo deployment and give some basic ideas on how to create a forgejo actions workflow touching some basics on the way.

Integrating the runner with the forgejo deployment

Consider there is already a working k3s forgejo like the one created by c4k-forgejo or forgejos k8s cluster . We will want to integrate the runner setup without much disturbance of the forgejo deployment. We'll setup the runner with a docker daemon to run Docker in Docker (DIND) - this will allow the runner to start different containers at our leisure.

To get the runner to communicate with the forgejo instance and vice versa the runner needs to be registered with the instance. This requires the shared secret, runner name and the forgejo instance address. Registering on the runner side requires us to wait for the docker daemon and to create the runner file which creates the aforementioned variables. Registration on the instance side will only require runner name and the shared secret.

Not modifying the existing forgejo deployment means we'll create a kubernetes job that waits for the forgejo deployment to be in healthy state and then execute a small registration script. We'll create the following resources to integrate with our existing forgejo deployment:

  • Runner Configmap
  • Runner Secret
  • Runner Deployment
  • Runner Service
  • Runner SetUp job

Notes for the Runner Configmap and Secret

You can find the configmap here. It contains the config.yaml file needed by the runner and the runner-id field. RUNNER_ID should be replaced with a sensible value that fits your semantics.

You can find the secret here. The secret holds the shared secret as stringData - replace RUNNER_SECRET with an appropriate value. Make sure to give your runner proper labels, otherwise it will not be able to pickup jobs. We used labels: ["ubuntu-latest:docker://ubuntu:latest"] for ours.

Note: The shared secret needs to be exactly 40 characters long and only consist of hexadecimal digits. Using openssl you could do: openssl rand -hex 40.

Notes for Runner Deployment

See the deployment yaml here. The FORGEJO_SERVICE_URL string needs to be replaced with the name of the forgejo service and the port of the pod it forwards to. Notice the runner containers command args. This is a script that will wait for the docker daemon to become ready, create the runner file which contains runner name, token and forgejo instance address. Then the runner is started in daemon mode.

The daemon container will be started with the docker-dind image. This allows the runner to pull and start docker containers for different kinds of jobs. The runner config is a volumeMount to a file under /conf. The runner deployment has the following internal architecture. Consider "localhost" to be the pod that host the runner and the DIND container.

block-beta
    columns 1
    Server:1
    space:1
    Node:1
    space:1
    Pod["Runner Deployment Pod"]:1
    id1{{"Container can access internally via localhost"}}:1
    space:1
    block:cc
        c1["Runner Container"]
        c2["DIND Container"]
    end
    
    Server -- "hosts" --> Node
    Node -- "hosts" --> Pod
    Pod -- "hosts" --> c1
    Pod -- "hosts" --> c2

Notes for the Runner Service

You can find the service definition here. It is plainly a service definition to allow the forgejo instance to find the runner and it is not exposing the runner to the outside world.

Notes for batch job

Find the setup batch job here. This is the job that will do registration of the runner on the instance side. The FORGEJO_SERVICE_URL string needs to be replaced with the name of the forgejo service and the port of the pod it forwards to.

Note the command args of the forgejo container. This is the script that will be run when the jobs starts. First the script will wait until the forgejo instance is reachable under the given FORGEJOINSTANCEURL. Then registration will be attempted using the runner name provided by the configmap-runner.yaml and the secret stored in secret-runner.yaml. The job will thus run exactly once during the startup phase of the runner deployment.

See also

  • Runner Kubernetes Resource Examples: https://code.forgejo.org/forgejo/runner/src/branch/main/examples/kubernetes/dind-docker.yaml
  • Runner Config: https://forgejo.org/docs/next/admin/runner-installation/#configuration
  • Labels: https://forgejo.org/docs/latest/admin/actions/#choosing-labels

Basic CI Setup

Caveats

  • When the workflow file is invalid, there will be no obvious error shown on the forgejo actions web-ui. Instead the workflow will just not appear on push. Errors can be followed in the forgejo logs though. Also when viewing the file directly in the web-ui, the user will be notified that the file is invalid.
  • The workflow file needs to exist on main to be picked up, even for branches and it needs to have triggers:
    name: build-website
    on:
        pull_request:
        push:
        workflow_dispatch:
    
  • If the access token does not have the correct permissions, uploading a package may fail silently (due to curl not failing on auth errors by default).

Getting And Using Actions

The forgejo actions code is hosted here.

#Example:
name: build-website

on:
  pull_request:
  push:
  workflow_dispatch:

jobs:
  build-site:
    runs-on: ubuntu-latest
    container:
      image: 'node:24.4'
    steps:
  • name: checkout-code uses: https://data.forgejo.org/actions/checkout@v4

Its worth checking out each actions repository as there are a lot of options on how to use the actions.

Secrets & Variables

Secrets and variables can be set on different levels:

  • Repository
  • Owner
  • Organisation

They can always be found in the settings under the "Runner" category. See secrets documentation here: https://forgejo.org/docs/latest/user/actions/basic-concepts/#secrets

# Accessing secrets and vars example
name: build-website

on:
  pull_request:
  push:
  workflow_dispatch:

jobs:
  build-site:
    runs-on: ubuntu-latest
    container:
      image: 'node:24.4'
    steps:
  • name: build env: USER: ${{ vars.WEBSITE_USER }} TOKEN: ${{ secrets.WEBSITE_USER_TOKEN }} run: | echo "Hi, ${USER} placing your token ..." echo "${TOKEN}" > ~/.token

Uploading Packages & Artifacts

Packages

Uploading packages will be done via respective package manager of the programming language. There is also a generic package registry which can be accessed with curl and proper authentication.

Packages are located in the package registry e.g. https://your-forgejo.org/api/packages/repo-owner/generic/package-name/package-version/package.zip (or any other filetype you chose to upload)

# Example upload to forgejo generic package registry
name: build

on:
  push:
    branches:
  • main jobs: build-site: env: USER: ${{ vars.USER }} TOKEN: ${{ secrets.TOKEN }} BASE_URL: "my.forgejo.instance" runs-on: ubuntu-latest container: image: 'node:24.4' steps: - name: Checkout Code uses: https://data.forgejo.org/actions/checkout@v4 - name: Build run: | echo "${{ env.USER }} at ${{ env.BASE_URL }}" echo "Build" make build - name: Upload as Package env: FILE: "build.zip" MY_ORG: "my-org" MY_REPO: "my-repo" run: | #!/bin/sh set -e zip -q -r ${FILE} my-build-dir version=$( git rev-parse --short HEAD ) path=api/packages/${MY_ORG}$/generic/${MY_REPO}$/${version} echo "Uploading Package with version: ${version} to https://${{ env.BASE_URL }}/${path}/${FILE}" status=$(curl --user ${{ env.USER }}:${{ env.TOKEN }} --upload-file ${FILE} "https://${{ env.BASE_URL }}/${path}/${FILE}" --write-out "\nStatus: %{http_code}\n" | grep -o "201") if [ ${status} != "201" ]; then echo "Upload failed, please check path and credentials"; exit 1; fi

Artifacts

Artifacts can be uploaded with the upload artifacts action. Note, that for self hosted runners v3 needs to be used. See Forgejo relevant info on uploading artifacts here: https://forgejo.org/docs/latest/user/actions/advanced-features/#artifacts. Artifacts are located under the respective pipeline run, e.g. https://your-forgejo.org/meissa/some-repo/actions/runs/24/artifacts/your-artifact.

# Example upload to forgejo artifact storage
name: build

on:
  push:
    branches:
  • main jobs: build-site: env: USER: ${{ vars.USER }} BASE_URL: "my.forgejo.instance" runs-on: ubuntu-latest container: image: 'node:24.4' steps: - name: Checkout Code uses: https://data.forgejo.org/actions/checkout@v4 - name: Build run: | echo "${{ env.USER }} at ${{ env.BASE_URL }}" echo "Build" make build - name: Upload as Artifact uses: https://data.forgejo.org/actions/upload-artifact@v3 with: name: my-build path: my-build-dir/ if-no-files-found: error retention-days: 120 overwrite: true

Conclusion

The runner setup is fairly straight forward even considering registration and waiting for the forgejo deployment to be ready. Once registered on both sides, the runner shows up in all the settings, remember, we set it as global. The way it is designed here means it can be set up independently from the forgejo deployment which allows you get it running even on an older (but compatible) instance.

Creating workflows comes with some caveats that you need to know but it is all manageable. They are kept very close to the workflows of a well known git platform. So if you're familliar with their actions, you'll feel right at home with forgejo actions.