Skip to content

2025

Learning to Leverage Gitea Runners

When I first started my journey with a GitOps mentality to transition a portion of my homelab's infrastructure to an "Intrastructure-as-Code" structure, I had made my own self-made Docker container that I called the Git-Repo-Updater. This self-made tool was useful to me because it copied the contents of Gitea repositories into bind-mounted container folders on my Portainer servers. This allowed me to set up configurations for Homepage-Docker, Material MkDocs, Traefik Reverse Proxy, and others to pull configuration changes from Gitea directly into the production servers, causing them to hot-load the changes instantly. (within 10 seconds, give or take).

Criticisms of Git-Repo-Updater

When I made the Git-Repo-Updater docker container stack, I ran into the issue of having made something I knew existing solutions existed for but simply did not understand well-enough to use yet. This caused me to basically delegate the GitOps workflow to a bash script with a few environment variables, running inside of an Alpine Linux container. While the container did it's job, it would occassionally have hiccups, caching issues, or repository branch errors that made no sense. This lack of transparency and the need to build an entire VSCode development environment to push new docker package updates to Gitea's package repository for Git-Repo-Updater caused a lot of development headaches.

Additional Concerns

In addition to the above, I had security concerns with the method of how I was interacting with the underlying container storage folders. I was running the containers as privileged: true, which was not safe nor secure if the container itself was breached (not likely, but you never know). I also didn't like the idea that was using my own CI/CD pipeline for something that I was certain existed elsewhere with much more hardening built-in, and I felt that I was intellectually "falling-behind" if I didn't figure out how to use them and migrate away from my Git-Repo-Updater project.

Introduction to Gitea Runners

When I finally got around to figuring out the general architecture of how Gitea Act "Runners" operate, it seemed so much more intuitive than the method I was using in Git-Repo-Updater. The runners can run as a standalone packages, docker containers, or can exist in other forms. The general idea of how the Runners operate (within my specific GitOps code-to-server use-case) is as-follows:

  • You spin up a Gitea runner docker container on a server that can access the server files that need to be overwritten/modified
  • The Gitea runner container registers itself to the Gitea server using a registration token generated by a specific Gitea repository
  • The Gitea repository has a Gitea-specific workflows folder that holds .yaml files that the runner uses to define tasks that occur when the repository has changes made to it / commits pushed to it.
  • The runner checks out the repository (clones it to the runner environment), and leverages rsync to copy the data into the production server's configuration folder(s) based on the unique needs of the task.
  • The runner cleans up after itself and returns back to an "Idle" state.
  • The production server hot-loads the changed configuration files (e.g. Material MkDocs, Traefik, Nginx, etc) and the changes go to into effect immediately

Docker-Compose Runner Deployment

When it comes to deploying a runner, (assuming you want to use a docker-based runner) it has a few simple things that need to be configured, the docker-compose.yml and the .env files. These tell the runner to reach out to Gitea server to register the runner with the given repository that you generated a registration token on.

docker-compose.yml
version: "3.8"
services:
  app:
    image: docker.io/gitea/act_runner:latest
    environment:
      CONFIG_FILE: /config.yaml
      GITEA_INSTANCE_URL: "${INSTANCE_URL}"
      GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}"
      GITEA_RUNNER_NAME: "${RUNNER_NAME}"
      GITEA_RUNNER_LABELS: "gitea-runner-mkdocs" # This can be anything, and is referenced by the workflow task(s) later.
    volumes:
      - /srv/containers/gitea-runner-mkdocs/config.yaml:/config.yaml # You have to manually make this file before you start the container
      - /srv/containers/material-mkdocs/docs/docs:/Gitops_Destination # This is where the repository data will be copied to
.env
INSTANCE_URL=https://git.bunny-lab.io
RUNNER_NAME=gitea-runner-mkdocs
REGISTRATION_TOKEN=<Generated Here: https://git.bunny-lab.io/bunny-lab/docs/settings/actions/runners>

Creating the config.yaml

The oddball thing about the way that I configured the Gitea Act Runner was telling it to run the container in "host mode" which tells it to run the tasks / workflows directly on the container itself instead of spinning up an instanced container (referred to as "Docker-in-Docker"). This keeps things simpler, but requires us to add a line to the config.yaml located at /srv/containers/gitea-runner-mkdocs/config.yaml. You can use your preferred text editor to add the following to the file's contents. This tells the runner to use itself for the tasks instead of an instanced docker container.

/srv/containers/gitea-runner-mkdocs/config.yaml
container_engine: ""

Runner Workflow Task Files

When it comes to telling the runner what to do and how to do it, you create what are called runner "Workflows". These files reside within <RepoRoot>/.gitea/workflows and are .yaml format. If you have any familiarity with Ansible, the similarities are staggaring. You can have multiple workflows for one repository, with different flows that fire-off on different runners. An example of the flow used to replace Git-Repo-Updater's functionality can be seen below.

In the workflow below, it spins up a runner within the Alpine Linux environment that the docker.io/gitea/act_runner:latest uses, then installs NodeJS, Git, and Rsync for the core functionality that mirrors Git-Repo-Updater:

.gitea/workflows/gitops-automatic-deployment.yml
name: GitOps Automatic Deployment

on:
  push:
    branches: [ main ]

jobs:
  GitOps Automatic Deployment:
    runs-on: gitea-runner-mkdocs

    steps:
      - name: Install Node.js, git, and rsync
        run: |
          apk add --no-cache nodejs npm git rsync

      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Copy Repository Data to Production Server
        run: |
          rsync -a --delete --exclude='.git/' . /Gitops_Destination/

runs-on Variable

In this example workflow file, we are targeting the previously-mentioned gitea-runner-mkdocs runner, which we gave that "label" in the docker-compose.yaml file's GITEA_RUNNER_LABELS variable. You can name these labels whatever you want, as a way of organizing which runners run which workflows associated with a repository when changes are made to the repository.

It All Comes Together

When all of this is set up, it works exactly like Git-Repo-Updater did, but more securely, faster, and more robustly, as the tasks can be changed from the repository-level instead of having to make changes inside of a Dockerfile or having to learn how to publish your own Docker containers to a registry. When you learn how to do it, it becomes faster and easier to set up than Git-Repo-Updater as well.

Then, when you push changes to a repository, the workflow's task triggers automatically, and appears under the "Actions" section of Gitea, and copies the repository data to the production server's configuration folder, entirely on its own. The power of runners is only limited by your creativity and can rapidly accelerate not just GitOpts workflows, but other more advanced flows (I will research this more in the future).

Gitea Act Runners are a beautiful thing, and it's a damn shame it took me this long to get around to learning how they work and using them.

Gitea_Runner_Screenshot

Windows Power Profiles Causing Notable CPU Performance Loss

So I've been noticing a trend recently regarding something I never really took much time to consider, but later realized had huge potential to impact performance of potentially both physical and virtual servers. (I have not personally seen it affect virtual machines, but it's plausible it could happen).

Overview of the Problem

The general idea is that Windows devices (Workstations & Servers) have what are called power "profiles". These profiles, by default, are set to "Balanced". Which in basic terms means that the operating system will artificially limit the CPU speed to below 2.0GHz at all times. This means if the CPU is capable of 4GHz, it will be limited to 2GHz no-matter-what. This is a huge problem since it leaves performance just sitting on the table.

Observations & Actions Taken

When I learned of the above, I began to audit every Windows-based server and workstation (Physical and Virtual) in my homelab. The virtual machines seemed unaffected by this issue, but I still configured them to "High Performance" power profiles regardless. However, every single physical host (VIRT-NODE-01, VIRT-NODE-02, and LAB-DRAAS-01), all saw notable performance improvements ranging from 32% to 41%, on average going from 1.75GHz to 2.6GHz on the virtualization hosts, and 1.9GHz to 3.2GHz on the backup server.

Final Thoughts

I am so upset that for so many years, it never occured to me that the power profiles applied to server operating systems. I always just assumed they ran in "High Performance" power profiles all the time. I discovered I had non-trivial amounts of performance loss because of this simple checkbox setting in the OS.

Performance Improvements

The two Hyper-V Failover Cluster hosts saw a 32% performance improvement (1.75GHz to 2.6GHz), while the Veeam Backup & Replication Server host observed a whopping 42% performance improvement (1.9GHz to 3.2GHz).