Zum Inhalt

Gitea Runner role

Deployt Gitea-Actions-Runner-Container. Dieser Runner nutzt den basic-Flavour von gitea/runner (Alpine, kein eingebauter Docker-Daemon, Docker-Socket wird gemountet).

Das bestehende Netz gitea-runner erhält gw_priority: 1 (Compose >= 2.33.1). Der Runner verbindet sich während eines Jobs zusätzlich mit dessen Netz. Ohne feste Gateway-Priorität kann Docker dabei seine Standardroute wechseln; Gitea-Verbindungen brechen ab und Jobs bleiben nach erfolgreichen Schritten beim Cleanup hängen. Die feste Priorität hält die Standardroute im Runner-Netz.

Stand: Gitea 1.27, Gitea Runner v3.1.0 (Juli 2026)

Wichtige Begriffe

  • Runner – Container, der Jobs von der Gitea-Aktions-Engine empfängt und startet.
  • Job-Image – das Docker-Image, in dem ein Job läuft.
  • Label – Bindet ein runs-on: xyz an ein Image (oder den Host): xyz:docker://gitea.rwcloud.ch/… oder xyz:host.

⚡ Wichtig: runs-on: in Workflows wird mit den Labels des Runners gematcht. Wenn der Runner nur ubuntu-latest (ohne docker://…) als Label hat, läuft der Job direkt auf dem Host des Runner-Containers – nicht in einem eigenen Image!

Ein Job bekommt sein runs-on-Image durch die Label-Syntax:

1
2
3
4
5
# im Ansible-Inventory (group_vars):
tags:
  - java-25:docker://gitea.rwcloud.ch/homelab/runner-java-25-ubuntu-26.04:latest
  - shell:host                       # "host" = Job-Schritte laufen im Runner-Host
  - ansible:docker://gitea.rwcloud.ch/my-repos/ansible-runner
1
2
3
4
# im Workflow:
jobs:
  build:
    runs-on: java-25   # ← findet das Label, nutzt das Java-25-Image

Wenn kein Label zu runs-on passt, fällt der Runner auf docker.gitea.com/runner-images:ubuntu-latest zurück (extern!).

Runner-Image-Flavour

Tag Basis Docker-Daemon für Jobs Privilegien
gitea/runner:latest (basic) alpine extern (Host-Socket gemountet) keine, kein --privileged
gitea/runner:latest-dind docker:dind eingebaut, root --privileged
gitea/runner:latest-dind-rootless docker:dind-rootless eingebaut, UID 1000 rootless-Limits

Empfehlung: basic (Status Quo). Der Job-Daemon ist der Host-Daemon, docker image prune greift direkt – ideal für Auto-Patching (siehe unten).

Konfiguration – Beispiel

playbooks/proxmox/group_vars/gitea_runner.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
gitea_runners:
  # ── Java-25-Runner (Vaadin Flow, Kotlin, Maven, gitflow) ─────────────────
  - name: runner-java-25
    token: !vault |
      $ANSIBLE_VAULT;1.2;AES256;rwcloud
      <…>
    runner_image: gitea.rwcloud.ch/official-docker-images/gitea/runner:latest
    instance_url: https://gitea.rwcloud.ch
    tags:
      - java-25:docker://gitea.rwcloud.ch/homelab/runner-java-25-ubuntu-26.04:latest
      - java-25-oracle:docker://gitea.rwcloud.ch/homelab/runner-java-25-oraclelinux-9:latest
      - shell:host
    capacity: 2
    # Registry-Login einmalig (läuft IM Job-Container, braucht docker-CLI in Image)
    hooks:
      job_started: |
        set -e
        [ -n "$REGISTRY_PW" ] && \
          echo "$REGISTRY_PW" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
    envs:
      REGISTRY_HOST: gitea.rwcloud.ch
      REGISTRY_USER: builder
      http_proxy: http://proxy.example:3128
      https_proxy: http://proxy.example:3128
      no_proxy: gitea.rwcloud.ch,.rwcloud.ch
    post_task_script_body: |
      #!/bin/sh
      set -e
      docker image prune -f 2>/dev/null || true
      docker container prune -f 2>/dev/null || true
      docker network prune -f 2>/dev/null || true
      docker builder prune -f 2>/dev/null || true
    post_task_script_timeout: 5m
    state: present

  # ── Standard-Runner (Ubuntu, kein eigenes Image) ─────────────────────────
  - name: runner-generic
    token: !vault |
      $ANSIBLE_VAULT;1.2;AES256;rwcloud
      <…>
    tags:
      - ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-24.04
      - linux-amd64:host
    state: present

Inventory-Fields (pro Runner)

Feld Pflicht Default Beschreibung
name ✅ – Container-Name (unique)
token ✅ – Registrierungstoken aus Gitea-UI
tags ✅ – List of Labels (name oder name:docker://img oder name:host)
state ✅ – present / absent
runner_image – gitea/runner:latest Image des Runner-Containers (basic/dind/dind-rootless)
instance_url – https://gitea.rwcloud.ch URL der Gitea-Instanz
capacity – 1 Parallel-Jobs
timeout – 36h Job-Timeout
envs – – Dict, läuft in jeden Job als Env-Var
hooks.job_started – – Shell-Skript, vor erstem Step, im Job-Context
hooks.job_completed – – Shell-Skript, nach letztem Step, im Job-Context
post_task_script_body – – Shell-Skript, nach jedem Job, vom Runner ausgeführt. Wird auf den Host geschrieben (/etc/gitea-runner/<name>/post-task.sh), read-only in den Runner-Container gemountet und unter post_task_script: /usr/local/bin/gitea-post-task.sh konfiguriert. Braucht docker-CLI im Runner-Flavour (basic hat keins → -dind oder Custom-Image).
post_task_script_timeout – 5m Timeout für das Post-Skript
cache.dir – – Pfad für actions/cache
cache.external_server – – Gemeinsamer Cache-Server (http://…)
cache.external_secret – – Secret für Cache-Server
cache.v2 – true Cache-Service v2

Hinweis zu hooks: Hooks laufen im Job-Context – also im Job-Container bei docker://-Labels, auf dem Host bei host-Labels. docker login braucht docker-CLI im Job-Image. Alternativ: Runner-envs für Secrets, die immer gesetzt sein sollen (z. B. Registry-URL).

Workflow-Beispiel

.gitea/workflows/build-vadn.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: Vaadin Build

on:
  push:
    branches: [main, develop]
    paths: ["src/**", "pom.xml"]
  pull_request:
  workflow_dispatch:
    inputs:
      clean_install:
        description: "mvn clean before install"
        required: false
        default: true
        type: boolean

jobs:
  build:
    runs-on: java-25   # ← Label aus Inventory, nutzt das Java-25-Image
    steps:
      # Checkout kommt vom eigenen Mirror – kein github.com!
      - uses: https://gitea.rwcloud.ch/actions/checkout@v4

      - name: Show environment
        run: |
          java -version
          mvn -version
          kotlinc -version
          git flow version   # gitflow via git
          avh version        # gitflow-avh via npm

      - name: Build
        run: mvn -B -ntp -DskipTests clean package

      - name: Vaadin build (NPM via Vaadin-Plugin)
        run: mvn -ntp vaadin:prepare-frontend vaadin:build-frontend

      - name: Test
        if: ${{ inputs.clean_install != 'false' }}
        run: mvn -B -ntp test

Auto-Patch der Job-Images

Ziel: kein manuelles Rebuild, keine manuelle Label-Änderung im Ansible-Inventory.

Bausteine

  1. Image-Repo (z. B. docker-builds) – enthält alle Dockerfiles + Build-Workflows
  2. Renovate – PRs für neue ARG JAVA_VERSION, KOTLIN_VERSION, MAVEN_VERSION
  3. Build-Workflow – Trigger: push auf Dockerfile-Änderung + wöchentlich + manuell
  4. Registry – :latest + immutable Versionstags (Rollback)
  5. post_task_script – pruned Docker-Cache, damit Runner-Hosts nicht voller alt-Image-Schrott werden

Renovate-Konfiguration (ergänzend)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "customManagers": [
    {
      "customType": "regex",
      "fileMatch": ["^runner-java/.*Dockerfile$"],
      "matchStrings": [
        "ARG\\s+JAVA_VERSION\\s*=\\s*(?<currentValue>[^\\s]+)\\s*$",
        "ARG\\s+KOTLIN_VERSION\\s*=\\s*(?<currentValue>[^\\s]+)\\s*$",
        "ARG\\s+MAVEN_VERSION\\s*=\\s*(?<currentValue>[^\\s]+)\\s*$"
      ],
      "datasourceTemplate": "github-releases"
    }
  ]
}

Build-Workflow

.gitea/workflows/runner-java-build.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
name: Runner Java Image Build

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "latest only? false → auch versionstag"
        required: false
        default: "false"
  push:
    paths:
      - 'runner-java/**'
  schedule:
    - cron: "15 2 * * 4"  # Donnerstag 02:15 UTC

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - base: runner-java/ubuntu-26.04
            suffix: ubuntu-26.04
          - base: runner-java/oraclelinux-9
            suffix: oraclelinux-9
    steps:
      - uses: https://gitea.rwcloud.ch/actions/checkout@v4

      - name: Login to Registry
        run: |
          echo "${{ secrets.BUILDUSER_AUTHTOKEN }}" | \
            docker login gitea.rwcloud.ch -u "${{ secrets.BUILDUSER_USERNAME }}" --password-stdin

      - name: Build & push
        run: |
          IMG=gitea.rwcloud.ch/homelab/runner-java-25-${{ matrix.suffix }}
          VERSION_TAG=${{ inputs.tag == 'true' ? '' : "$GITHUB_RUN_NUMBER" }}

          docker build -f ${{ matrix.base }}/Dockerfile \
            --build-arg JAVA_VERSION=25 \
            --build-arg KOTLIN_VERSION=2.1.20 \
            --build-arg MAVEN_VERSION=3.9.9 \
            -t $IMG:latest \
            $([ -n "$VERSION_TAG" ] && echo "-t $IMG:$VERSION_TAG") \
            .

          docker push $IMG:latest
          [ -n "$VERSION_TAG" ] && docker push $IMG:$VERSION_TAG

Cache-Automatik im Runner

  • runner.post_task_script pruned nach jedem Job (docker image/container prune)
  • pull: always im docker_compose_v2 Task → Runner-latest-Image wird automatisch geupdatet
  • Job-Images werden vom Runner auf Job-Start gezogen (oder aus Local-Cache)

Achtung: Job-Images bleiben im Local-Daemon-Cache, bis prune sie entfernt. Wenn ein neuer latest-Build herauskommt und ein Job läuft, hält der laufende Job das alte Image. Neue Jobs ziehen das neue. Das ist gewolltes Verhalten – aber post_task_script pruned nur unreferenzierte Images, nicht laufende.

Gitea-Instanz: Actions ohne github.com

Damit uses: … nicht von github.com lädt:

  1. app.ini der Gitea-Instanz:
1
2
3
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = self   ; nur von eigener Gitea
  1. Action-Repositories spiegeln:
1
2
3
4
tea repos create --mirror \
  --remote-url "https://github.com/actions/checkout" \
  --path actions/checkout
# … für jede benutzte Action
  1. uses: entweder kurz (actions/checkout@v4) – dank Mirror + DEFAULT_ACTIONS_URL=self von eigener Gitea – oder absolut (https://gitea.rwcloud.ch/actions/checkout@v4).

  2. docker login ohne docker/login-action:

1
2
3
4
- name: Login to Registry
  env:
    REGISTRY_PW: ${{ secrets.BUILDUSER_AUTHTOKEN }}
  run: echo "$REGISTRY_PW" | docker login gitea.rwcloud.ch -u builder --password-stdin

Voraussetzung: Job-Label zeigt auf ein Image mit docker-CLI oder du nutzt runner.envs + hooks.job_started.

Monitoring

1
2
3
docker ps --filter "project=gitea-runner"
docker logs -f runner-java-25
docker inspect runner-java-25   # Mounts, Env-Vars

Gitea-UI: Repo → Actions → Runners zeigt aktive Runner + Labels.

Security

  • Runner-Container läuft root im basic/dind-Flavour (rootless dind → UID 1000).
  • /var/run/docker.sock ist ein Docker-Daemon-Zugang (Root-Äquivalent) – nur auf vertrauenswürdigen Hosts.
  • hooks.job_started läuft im Job-Context – Secrets hier nutzen ist bewusst.
  • post_task_script wird vom Runner in dessen Umgebung ausgeführt und bindet denselben Docker-Socket – hat also Daemon-Rechte.

Troubleshooting

Problem Ursache Fix
Job läuft nicht im richtigen Image Label-Syntax falsch (ubuntu-latest statt ubuntu-latest:docker://…) Label in Inventory mit docker://-Schema
java/mvn nicht gefunden Job-Image ohne Java Temurin in Dockerfile installieren
actions/checkout lädt von github.com DEFAULT_ACTIONS_URL nicht gesetzt [actions] DEFAULT_ACTIONS_URL=self + Mirror
docker login schlägt fehl Kein docker-CLI im Job-Image Docker-Image erweitern oder envs verwenden
Runner nimmt neue Labels nicht an .runner-File hat alten Label-State runner neu registrieren ODER config set runner.labels …
Cache hält altes Image Local Daemon-Cache post_task_script → docker image prune, oder pull: always
job_started-Hook scheitert docker-CLI fehlt im Job-Image docker-CLI installieren ODER Host-Label nutzen

Playbooks

1
2
3
4
5
6
7
8
9
# Playbook: playbooks/proxmox/deploy_runner01.yml

# Runner deploy (alle runners mit state: present):
ansible-playbook -i inventory/hosts.yml --vault-password-file ./.vault_pass \
  playbooks/proxmox/deploy_runner01.yml --tags runner-install

# Runner entfernen (alle runners mit state: absent):
ansible-playbook -i inventory/hosts.yml --vault-password-file ./.vault_pass \
  playbooks/proxmox/deploy_runner01.yml --tags runner-uninstall

Weiterführende Quellen