Files
moneyfyandClaude Opus 5 87bee9b72a build: Docker-Images, Compose-Setup und Gitea-Workflows
- Mehrstufige Dockerfiles je Dienst mit Stufen für Entwicklung und Betrieb,
  beide Images laufen als unprivilegierter Benutzer
- Backend-Entrypoint wartet auf die Datenbank, migriert und seedt nur bei
  leerem Kategoriebaum (neues Flag --if-empty)
- docker-compose.yml mit Netz-Trennung, Healthchecks und benannten Volumes;
  die Datenbank ist ausschließlich im internen Netz erreichbar
- nginx liefert das Bundle aus und reicht /api weiter; index.html ungecacht,
  gehashte Assets ein Jahr
- Gitea-Workflows: ci.yml für Lint, Tests und Bundle-Bau, build.yml für die
  Images nach linux/amd64 ohne Deploy-Schritt
- docs/runner.md und docs/deployment.md, .dockerignore je Dienst

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
2026-09-09 17:14:44 +02:00

8.0 KiB
Raw Permalink Blame History

Gitea-Actions-Runner einrichten

Die Workflows unter .gitea/workflows/ brauchen einen act_runner, der Docker erreichen kann. Diese Anleitung richtet ihn als Container auf docker-srv001 ein. Sie ist vollständig von der Tokenbeschaffung bis zur Fehlersuche.

1. Registrierungstoken holen

Es gibt zwei Möglichkeiten:

Nur für dieses Repository (empfohlen, wenn der Runner ausschließlich moneyfy bauen soll):

  1. In Gitea das Repository menzeljonas/moneyfy öffnen.
  2. Settings → Actions → Runners aufrufen.
  3. Create new Runner anklicken. Gitea zeigt das Registrierungstoken an eine Zeichenkette aus etwa 40 Zeichen.
  4. Token kopieren. Es lässt sich nur einmal verwenden.

Instanzweit (der Runner steht dann allen Repositories zur Verfügung):

  1. Als Administrator anmelden.
  2. Site Administration → Actions → Runners aufrufen.
  3. Ebenfalls Create new Runner, Token kopieren.

Falls Actions in der Instanz noch abgeschaltet sind, in der app.ini des Gitea-Servers ergänzen und Gitea neu starten:

[actions]
ENABLED = true

2. Runner als Container betreiben

Auf docker-srv001 ein eigenes Verzeichnis anlegen:

mkdir -p /opt/gitea-runner/data
cd /opt/gitea-runner

docker-compose.yml:

name: gitea-runner

services:
  act-runner:
    image: gitea/act_runner:0.2.11
    container_name: gitea-act-runner
    restart: unless-stopped
    environment:
      GITEA_INSTANCE_URL: https://git.menzel.center
      GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
      GITEA_RUNNER_NAME: docker-srv001
      # Muss zu den Labels in config.yaml passen.
      GITEA_RUNNER_LABELS: ubuntu-latest,ubuntu-22.04
      CONFIG_FILE: /config.yaml
    volumes:
      # Der Runner startet die Job-Container über den Docker-Socket des Hosts.
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config.yaml:/config.yaml:ro
      - ./data:/data

.env daneben:

GITEA_RUNNER_REGISTRATION_TOKEN=<Token aus Schritt 1>

Der gemountete Docker-Socket gibt dem Runner faktisch Root-Rechte auf dem Host. Das ist für einen Runner, der eigene Images baut, üblich betreibe ihn deshalb nur mit vertrauenswürdigen Repositories.

3. config.yaml

Im selben Verzeichnis anlegen:

log:
  level: info

runner:
  file: /data/.runner
  capacity: 2
  timeout: 3h
  # Die linke Seite ist das Label aus `runs-on`, rechts das Image, in dem der Job läuft.
  labels:
    - "ubuntu-latest:docker://node:20-bookworm"
    - "ubuntu-22.04:docker://node:20-bookworm"

cache:
  enabled: true
  dir: /data/cache
  # Adresse, unter der die Job-Container den Cache-Server des Runners erreichen.
  # Leer lassen heißt: automatisch ermitteln. Bei mehreren Netzen hier die IP
  # des Hosts im Runner-Netz eintragen.
  host: ""
  port: 0

container:
  # Job-Container in dasselbe Netz hängen wie der Runner.
  network: bridge
  privileged: false
  # Nötig, damit `actions/cache` und `docker/build-push-action` Pfade einblenden dürfen.
  valid_volumes:
    - /data/cache
  docker_host: "-"
  # Bei selbstsigniertem Zertifikat auf true setzen, siehe Abschnitt 6.
  force_pull: false

docker_host: "-" bedeutet: den Socket des Runners an die Job-Container weiterreichen. Genau das braucht build.yml, um Images zu bauen.

Starten:

docker compose up -d
docker compose logs -f

Beim ersten Start registriert sich der Runner und legt /data/.runner an. Ab dann wird das Registrierungstoken nicht mehr gebraucht.

4. Repo-Secret REGISTRY_TOKEN anlegen

Der Build-Workflow lädt Images in die Gitea-Registry und braucht dafür ein Token mit Schreibrecht auf Pakete:

  1. In Gitea auf Benutzereinstellungen → Applications → Generate New Token.
  2. Namen vergeben, etwa moneyfy-registry.
  3. Unter Select permissions bei package auf Read and Write stellen.
  4. Generate Token, Wert kopieren er wird nur einmal angezeigt.
  5. Im Repository Settings → Actions → Secrets → Add Secret:
    • Name: REGISTRY_TOKEN
    • Wert: das kopierte Token

Der Workflow meldet sich mit ${{ github.actor }} an, also mit dem Benutzer, der den Lauf ausgelöst hat. Dieser Benutzer muss Schreibrecht auf die Pakete des Namensraums menzeljonas haben.

5. Verifikation

  1. Runner erscheint als „idle“: in Gitea unter Settings → Actions → Runners. Dort stehen Name (docker-srv001), Labels und Status.

  2. Testworkflow läuft: einen Commit auf einen beliebigen Branch schieben. Unter Actions muss CI starten und beide Jobs grün abschließen.

  3. Image landet in der Registry: auf main pushen und danach https://git.menzel.center/menzeljonas/-/packages öffnen. Dort müssen moneyfy-backend und moneyfy-frontend mit den Tags main und sha-<kurz> auftauchen.

  4. Versions-Tag prüfen:

    git tag -a v0.1.0 -m "Erste Version"
    git push origin v0.1.0
    

    Danach müssen zusätzlich 0.1.0, 0.1 und latest vorhanden sein.

  5. Image von Hand ziehen (prüft nebenbei die Registry-Anmeldung):

    docker login git.menzel.center
    docker pull git.menzel.center/menzeljonas/moneyfy-backend:latest
    

6. Fehlersuche

docker: command not found im Job

Das Job-Image bringt keinen Docker-Client mit. Zwei Wege:

  • In config.yaml sicherstellen, dass docker_host: "-" gesetzt ist der Runner reicht dann seinen Socket weiter.
  • Für Jobs, die Docker brauchen, ein Image mit Client verwenden, etwa das Label ubuntu-latest:docker://catthehacker/ubuntu:act-22.04. Die Actions docker/setup-buildx-action und docker/build-push-action bringen den Rest mit.

Prüfen lässt sich das mit einem Wegwerf-Schritt:

- run: docker version

Registry-Anmeldung schlägt mit selbstsigniertem Zertifikat fehl

Meldung: x509: certificate signed by unknown authority.

Auf dem Host das CA-Zertifikat hinterlegen und den Docker-Daemon neu starten:

mkdir -p /etc/docker/certs.d/git.menzel.center
cp menzel-center-ca.crt /etc/docker/certs.d/git.menzel.center/ca.crt
systemctl restart docker

Zusätzlich muss der Runner-Container die CA kennen, weil buildx im Container läuft. Im Compose des Runners einblenden:

    volumes:
      - /usr/local/share/ca-certificates/menzel-center-ca.crt:/usr/local/share/ca-certificates/menzel-center-ca.crt:ro

und im Runner einmalig update-ca-certificates ausführen. Ein gültiges Let's-Encrypt-Zertifikat auf dem Gitea-Host erspart diesen ganzen Abschnitt.

DOCKER_HOST im DinD-Betrieb

Wer statt des Host-Sockets einen Docker-in-Docker-Dienst betreibt, ergänzt diesen als zweiten Service und setzt im Runner:

    environment:
      DOCKER_HOST: tcp://docker-in-docker:2376
      DOCKER_CERT_PATH: /certs/client
      DOCKER_TLS_VERIFY: "1"
    volumes:
      - ./certs:/certs

In config.yaml dann docker_host: tcp://docker-in-docker:2376. DinD ist langsamer und der Schichten-Cache geht bei jedem Neustart verloren der gemountete Host-Socket ist für ein Homelab die pragmatischere Wahl.

Der Cache des Runners frisst Speicherplatz

/opt/gitea-runner/data/cache und die Build-Schichten wachsen mit jedem Lauf. Belegung ansehen:

du -sh /opt/gitea-runner/data/cache
docker system df

Aufräumen:

# Nicht mehr referenzierte Schichten und Build-Cache älter als eine Woche
docker buildx prune --filter "until=168h" -f
docker image prune -f

# Cache-Verzeichnis des Runners leeren
docker compose -f /opt/gitea-runner/docker-compose.yml down
rm -rf /opt/gitea-runner/data/cache/*
docker compose -f /opt/gitea-runner/docker-compose.yml up -d

Regelmäßig per Cron:

0 4 * * 0 docker buildx prune --filter "until=168h" -f >/dev/null 2>&1

Job bleibt in „waiting“ hängen

Die Labels stimmen nicht überein. runs-on: ubuntu-latest im Workflow braucht ein Label ubuntu-latest am Runner. Nach einer Änderung in config.yaml muss der Runner neu registriert werden:

docker compose down
rm data/.runner
# neues Registrierungstoken in .env eintragen
docker compose up -d