Self-hosted Forgejo runner in Kubernetes

Self-hosted Forgejo runner in Kubernetes
created by Google Nano Banana 2

In your life as professional software developer you want to have a sourcecode management system and some kind of automated build of your software bundles.

In the old days we often used a Git repository in combination with Jenkins as build system. Jenkins was divided up to a Jenkins Master and several Jenkins agents. Often it was a manual setup and adding building resources - Jenkins agents - took time and the setup did not scale very well.

Today in a Kubernetes world the sourcecode management system and the build system is combined into one product. GitHub and GitHub actions is an often used example.

But what if we want to have more control having such a system on premise? No problem, a self hosted Forgejo as sourcecode repository can be your friend. The build system is compatible to GitHub actions.

The setup is not too complicated. It consists of a Forgejo installation (I did this in docker) and for the build actions we need build runners doing all the work. In my case I deployed the runners in Kubernetes.

The Forgejo installation itself is well documented but the runner installation in Kubernetes was a little bit fiddly for me, especially if you need to be able to run docker inside the Forgejo runner. So here is my setup approach.

Kubernetes Forgejo Runner setup

step-by-step

All steps can be conbined into one Kubernetes yaml deployment file.

Step 1: Kubernetes namespace

I wanted to put the forgejo runner into a separate namespace in my Kubernetes.

kind: Namespace
apiVersion: v1
metadata:
  name: forgejo

Step 2: add runner token

The runner connects to forgejo. The authentication is done by token which can be created in Forgejo itself: https://forgejo.org/docs/next/admin/actions/registration/#interactive-registration

This token needs to be base64 encoded.

base64 <<< "forgejo runner token"

The result needs to be put into a kubernetes secret.

apiVersion: v1
data:
  # The registration token can be obtained from the web UI, API or command-line.
  # You can also set a pre-defined global runner registration token for the Gitea instance via
  # `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
  token: {base64 encoded forgejo token}
kind: Secret
metadata:
  name: gitea-runner-secret
  namespace: forgejo
type: Opaque
💡
ensurce to set your token!

Step 3: add Forgejo configuration

To configure Forgejo we create a ConfigMap containing the configuration.yaml file.

This configuration contains the settings i.e. for Docker and the available base build images.

apiVersion: v1
kind: ConfigMap
metadata:
  name: gitea-act-runner-config
  namespace: forgejo
  annotations:
    reloader.stakater.com/auto: "true"
data:
  config.yaml: |-
    log:
      # The level of logging, can be trace, debug, info, warn, error, fatal
      level: debug

    runner:
      # Where to store the registration result.
      file: .runner
      # Execute how many tasks concurrently at the same time.
      capacity: 1
      # Extra environment variables to run jobs.
      envs:
        A_TEST_ENV_NAME_1: a_test_env_value_1
        A_TEST_ENV_NAME_2: a_test_env_value_2
      # Extra environment variables to run jobs from a file.
      # It will be ignored if it's empty or the file doesn't exist.
      env_file: .env
      # The timeout for a job to be finished.
      # Please note that the Gitea instance also has a timeout (3h by default) for the job.
      # So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
      timeout: 30m
      # Whether skip verifying the TLS certificate of the Gitea instance.
      insecure: false
      # The timeout for fetching the job from the Gitea instance.
      fetch_timeout: 5s
      # The interval for fetching the job from the Gitea instance.
      fetch_interval: 2s
      # The labels of a runner are used to determine which jobs the runner can run, and how to run them.
      # Like: "macos-arm64:host" or "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
      # Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
      # If it's empty when registering, it will ask for inputting labels.
      # If it's empty when execute `daemon`, will use labels in `.runner` file.
      labels:
        - "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
        - "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
        - "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04"

    cache:
      # Enable cache server to use actions/cache.
      enabled: true
      # The directory to store the cache data.
      # If it's empty, the cache data will be stored in $HOME/.cache/actcache.
      dir: ""
      # The host of the cache server.
      # It's not for the address to listen, but the address to connect from job containers.
      # So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
      host: ""
      # The port of the cache server.
      # 0 means to use a random available port.
      port: 0
      # The external cache server URL. Valid only when enable is true.
      # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
      # The URL should generally end with "/".
      external_server: ""

    container:
      # Specifies the network to which the container will connect.
      # Could be host, bridge or the name of a custom network.
      # If it's empty, act_runner will create a network automatically.
      network: ""
      # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
      privileged: false
      # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
      options: "--add-host=docker:host-gateway -v /certs:/certs -e DOCKER_HOST=tcp://docker:2376 -e DOCKER_CERT_PATH=/certs/client -e DOCKER_TLS_CERTDIR=/certs -e DOCKER_TLS_VERIFY=1"
      # The parent directory of a job's working directory.
      # NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
      # If the path starts with '/', the '/' will be trimmed.
      # For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
      # If it's empty, /workspace will be used.
      # workdir_parent:
      # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
      # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
      # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
      # valid_volumes:
      #   - data
      #   - /src/*.json
      # If you want to allow any volume, please use the following configuration:
      # valid_volumes:
      #   - '**'
      valid_volumes:
        - /certs
      # overrides the docker client host with the specified one.
      # If it's empty, act_runner will find an available docker host automatically.
      # If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
      # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
      # docker_host: ""
      # Pull docker image(s) even if already present
      # force_pull: true
      # Rebuild docker image(s) even if already present
      # force_rebuild: false

    host:
    # The parent directory of a job's working directory.
    # If it's empty, $HOME/.cache/act/ will be used.
    # workdir_parent:

Step 4: add runner itself

The runner itself is deployed as StatefulSet. Inside the configuration you need to set the URL of your Forgejo instance.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: gitea-act-runner-dind
  name: gitea-act-runner-dind
  namespace: forgejo
  annotations:
    reloader.stakater.com/auto: "true"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gitea-act-runner-dind
  serviceName: gitea-act-runner-dind
  template:
    metadata:
      labels:
        app: gitea-act-runner-dind
        namespace: forgejo
    spec:
      restartPolicy: Always
      containers:
        - name: runner
          image: gitea/act_runner:nightly
          command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]

          env:
            - name: DOCKER_HOST
              value: tcp://localhost:2376
            - name: DOCKER_CERT_PATH
              value: /certs/client
            - name: DOCKER_TLS_VERIFY
              value: "1"
            - name: CONFIG_FILE
              value: /config.yaml
            - name: GITEA_INSTANCE_URL
              value: https://{your forgejo domain}
            - name: GITEA_RUNNER_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: GITEA_RUNNER_REGISTRATION_TOKEN
              valueFrom:
                secretKeyRef:
                  name: gitea-runner-secret
                  key: token

          volumeMounts:
            - name: docker-certs
              mountPath: /certs
            - name: gitea-runner-storage
              mountPath: /data
            - name: config
              mountPath: /config.yaml
              subPath: config.yaml

        - name: daemon
          image: docker:dind
          env:
            - name: DOCKER_TLS_CERTDIR
              value: /certs
          securityContext:
            privileged: true
          volumeMounts:
            - name: docker-certs
              mountPath: /certs

      volumes:
        - name: docker-certs
          emptyDir: {}
        - name: config
          configMap:
            name: gitea-act-runner-config

  volumeClaimTemplates:
    - apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: gitea-runner-storage
        namespace: forgejo
      spec:
        storageClassName: local-path
        accessModes: [ "ReadWriteOnce" ]
        resources:
          requests:
            storage: "1Gi"

Step 5: check installation

After you deployed all 4 steps in your Kubernetes the Forgejo runner registers to Forgejo and you will see an registered but inactive runner.

Forgejo Runner settings with available runner

Now you are ready to create build workflows in your Git projects.