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:
@@ -0,0 +1,212 @@
|
||||
# Betrieb auf docker-srv001
|
||||
|
||||
## Erstinstallation
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/moneyfy && cd /opt/moneyfy
|
||||
|
||||
# docker-compose.yml und .env.example aus dem Repository holen
|
||||
curl -fsSLO https://git.menzel.center/menzeljonas/moneyfy/raw/branch/main/docker-compose.yml
|
||||
curl -fsSLO https://git.menzel.center/menzeljonas/moneyfy/raw/branch/main/.env.example
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
In `.env` mindestens setzen:
|
||||
|
||||
```bash
|
||||
POSTGRES_PASSWORD=$(openssl rand -hex 24)
|
||||
SECRET_KEY=$(openssl rand -hex 32)
|
||||
MONEYFY_ADMIN_PASSWORD=<Startpasswort>
|
||||
PUBLIC_BASE_URL=https://moneyfy.menzel.center
|
||||
TAG=latest
|
||||
```
|
||||
|
||||
Das externe Proxy-Netz muss existieren – NPMplus bringt es üblicherweise mit:
|
||||
|
||||
```bash
|
||||
docker network inspect proxy >/dev/null 2>&1 || docker network create proxy
|
||||
```
|
||||
|
||||
An der Registry anmelden und starten:
|
||||
|
||||
```bash
|
||||
docker login git.menzel.center
|
||||
docker compose up -d
|
||||
docker compose logs -f moneyfy-backend
|
||||
```
|
||||
|
||||
Das Backend wartet auf die Datenbank, spielt die Migrationen ein und legt beim
|
||||
allerersten Start den Kategoriebaum an. Danach ist die Oberfläche unter
|
||||
`http://127.0.0.1:8087` erreichbar.
|
||||
|
||||
Beim ersten Anmelden gelten `MONEYFY_ADMIN_USER` und `MONEYFY_ADMIN_PASSWORD`;
|
||||
die Anwendung verlangt sofort ein neues Passwort. Danach kann
|
||||
`MONEYFY_ADMIN_PASSWORD` aus der `.env` entfernt werden.
|
||||
|
||||
## NPMplus konfigurieren
|
||||
|
||||
`moneyfy.menzel.center` als **Proxy Host** anlegen:
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Domain Names | `moneyfy.menzel.center` |
|
||||
| Scheme | `http` |
|
||||
| Forward Hostname / IP | `moneyfy-frontend` |
|
||||
| Forward Port | `8080` |
|
||||
| Cache Assets | aus – der Container setzt die Cache-Kopfzeilen selbst |
|
||||
| Block Common Exploits | an |
|
||||
| Websockets Support | aus – die Anwendung braucht keine |
|
||||
|
||||
Unter *SSL*: Zertifikat über Let's Encrypt anfordern, **Force SSL** und **HTTP/2**
|
||||
einschalten, **HSTS** aktivieren.
|
||||
|
||||
Damit NPMplus den Container über seinen Namen erreicht, müssen beide im Netz
|
||||
`proxy` liegen. Das erledigt `docker-compose.yml` bereits; prüfen mit:
|
||||
|
||||
```bash
|
||||
docker network inspect proxy --format '{{range .Containers}}{{.Name}} {{end}}'
|
||||
```
|
||||
|
||||
Unter *Advanced* diese Zusatzkonfiguration eintragen. Sie sorgt dafür, dass die
|
||||
Anwendung das richtige Schema sieht und Exporte nicht in einen Timeout laufen:
|
||||
|
||||
```nginx
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
client_max_body_size 4m;
|
||||
proxy_read_timeout 120s;
|
||||
```
|
||||
|
||||
> Die Anmeldung setzt `Secure`-Cookies. Ohne HTTPS – also ohne Zugriff über
|
||||
> NPMplus – kommt keine Sitzung zustande. Für einen Test über
|
||||
> `http://127.0.0.1:8087` muss `COOKIE_SECURE=false` gesetzt werden.
|
||||
|
||||
## Aktualisieren
|
||||
|
||||
Das Ausrollen passiert bewusst von Hand; die CI baut nur die Images.
|
||||
|
||||
```bash
|
||||
cd /opt/moneyfy
|
||||
|
||||
# Bei fester Version: TAG in .env auf die neue Version setzen.
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker compose logs -f moneyfy-backend
|
||||
```
|
||||
|
||||
Die Migrationen laufen beim Start des Backends automatisch. Ein Blick in die
|
||||
Ausgabe zeigt, welche Revision eingespielt wurde.
|
||||
|
||||
Zurückrollen: `TAG` auf die vorherige Version setzen und erneut
|
||||
`docker compose up -d`. **Achtung:** Migrationen werden dabei nicht rückgängig
|
||||
gemacht. Vor einem Update mit Schemaänderung immer erst sichern.
|
||||
|
||||
## Sicherung
|
||||
|
||||
Zwei Dinge müssen gesichert werden: die Datenbank und das Volume mit den Logos.
|
||||
|
||||
### Datenbank
|
||||
|
||||
```bash
|
||||
docker compose exec -T moneyfy-db \
|
||||
pg_dump -U moneyfy -d moneyfy --format=custom \
|
||||
> /var/backups/moneyfy/db-$(date +%F).dump
|
||||
```
|
||||
|
||||
Wiederherstellen:
|
||||
|
||||
```bash
|
||||
docker compose stop moneyfy-backend
|
||||
docker compose exec -T moneyfy-db \
|
||||
pg_restore -U moneyfy -d moneyfy --clean --if-exists < /var/backups/moneyfy/db-2026-03-01.dump
|
||||
docker compose start moneyfy-backend
|
||||
```
|
||||
|
||||
### Logo-Volume
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v moneyfy-data:/data:ro \
|
||||
-v /var/backups/moneyfy:/backup \
|
||||
alpine tar czf /backup/logos-$(date +%F).tar.gz -C /data .
|
||||
```
|
||||
|
||||
Wiederherstellen:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v moneyfy-data:/data \
|
||||
-v /var/backups/moneyfy:/backup \
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/logos-2026-03-01.tar.gz -C /data"
|
||||
```
|
||||
|
||||
Die Logos lassen sich zur Not auch neu beschaffen – die Datenbank nicht.
|
||||
|
||||
### Tägliche Sicherung per Cron
|
||||
|
||||
`/opt/moneyfy/backup.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ZIEL=/var/backups/moneyfy
|
||||
TAG=$(date +%F)
|
||||
mkdir -p "$ZIEL"
|
||||
|
||||
cd /opt/moneyfy
|
||||
docker compose exec -T moneyfy-db \
|
||||
pg_dump -U moneyfy -d moneyfy --format=custom > "$ZIEL/db-$TAG.dump"
|
||||
|
||||
docker run --rm \
|
||||
-v moneyfy-data:/data:ro \
|
||||
-v "$ZIEL:/backup" \
|
||||
alpine tar czf "/backup/logos-$TAG.tar.gz" -C /data .
|
||||
|
||||
# Älteres als 30 Tage entfernen.
|
||||
find "$ZIEL" -name 'db-*.dump' -mtime +30 -delete
|
||||
find "$ZIEL" -name 'logos-*.tar.gz' -mtime +30 -delete
|
||||
```
|
||||
|
||||
```cron
|
||||
30 3 * * * /opt/moneyfy/backup.sh >> /var/log/moneyfy-backup.log 2>&1
|
||||
```
|
||||
|
||||
Die Sicherungen anschließend auf ein anderes System spiegeln – eine Kopie auf
|
||||
demselben Host hilft bei einem Plattenausfall nicht.
|
||||
|
||||
## Fehlersuche
|
||||
|
||||
**Backend startet nicht, Log endet bei „Warte auf die Datenbank“**
|
||||
Die Datenbank ist nicht gesund. `docker compose ps` und
|
||||
`docker compose logs moneyfy-db` prüfen; meist stimmt `POSTGRES_PASSWORD` in der
|
||||
`.env` nicht mit dem im Volume hinterlegten Passwort überein. Ein bereits
|
||||
angelegtes Volume übernimmt kein neues Passwort.
|
||||
|
||||
**`SECRET_KEY ist nicht gesetzt`**
|
||||
Die Anwendung verweigert in Produktion den Start mit dem Platzhalterwert.
|
||||
`openssl rand -hex 32` erzeugt einen passenden Schlüssel. Wird er später
|
||||
geändert, sind alle Sitzungen ungültig – das ist beabsichtigt.
|
||||
|
||||
**Anmeldung schlägt fehl, obwohl das Passwort stimmt**
|
||||
Meist fehlt HTTPS: Die Cookies sind `Secure` gesetzt und erreichen den Server
|
||||
über eine reine HTTP-Verbindung nicht. Entweder über NPMplus zugreifen oder
|
||||
`COOKIE_SECURE=false` setzen.
|
||||
|
||||
**Firmen bekommen kein Logo**
|
||||
Ohne Netzzugang findet nur der lokale Markenkatalog etwas; unbekannte Firmen
|
||||
erhalten einen erzeugten Avatar. Das ist so gewollt. Mit
|
||||
`docker compose exec moneyfy-backend python -c "from app.services.logos import simple_icon_index; print(simple_icon_index().version)"`
|
||||
lässt sich prüfen, ob der Katalog im Image liegt.
|
||||
|
||||
**Keine Benachrichtigungen**
|
||||
`SCHEDULER_ENABLED` und `NOTIFICATIONS_ENABLED` prüfen, dann in der Oberfläche
|
||||
unter *Einstellungen → Benachrichtigungen* den Einrichtungsstand ansehen und
|
||||
eine Testnachricht schicken. Das Versandprotokoll steht auf derselben Seite.
|
||||
|
||||
**Speicherplatz**
|
||||
```bash
|
||||
docker system df
|
||||
docker compose exec moneyfy-db psql -U moneyfy -d moneyfy -c "\l+"
|
||||
docker run --rm -v moneyfy-data:/data:ro alpine du -sh /data
|
||||
```
|
||||
+270
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user