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
This commit is contained in:
moneyfy
2026-09-09 17:14:44 +02:00
co-authored by Claude Opus 5
parent 54c59c9f71
commit 87bee9b72a
17 changed files with 1184 additions and 10 deletions
+270
View File
@@ -0,0 +1,270 @@
# 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:
```ini
[actions]
ENABLED = true
```
## 2. Runner als Container betreiben
Auf `docker-srv001` ein eigenes Verzeichnis anlegen:
```bash
mkdir -p /opt/gitea-runner/data
cd /opt/gitea-runner
```
`docker-compose.yml`:
```yaml
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:
```bash
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:
```yaml
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:
```bash
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:**
```bash
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):
```bash
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:
```yaml
- 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:
```bash
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:
```yaml
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:
```yaml
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:
```bash
du -sh /opt/gitea-runner/data/cache
docker system df
```
Aufräumen:
```bash
# 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:
```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:
```bash
docker compose down
rm data/.runner
# neues Registrierungstoken in .env eintragen
docker compose up -d
```