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:
+6
-4
@@ -18,7 +18,9 @@ TAG=latest
|
||||
POSTGRES_DB=moneyfy
|
||||
POSTGRES_USER=moneyfy
|
||||
POSTGRES_PASSWORD=bitte-aendern
|
||||
# Vom Backend genutzte DSN. `postgresql://` wird automatisch auf asyncpg umgestellt.
|
||||
# Wird beim Betrieb mit Compose aus den drei Werten darüber zusammengesetzt und
|
||||
# muss dort nicht gesetzt werden. Nur für den Betrieb ohne Compose nötig.
|
||||
# `postgresql://` wird automatisch auf asyncpg umgestellt.
|
||||
DATABASE_URL=postgresql+asyncpg://moneyfy:bitte-aendern@moneyfy-db:5432/moneyfy
|
||||
|
||||
# --- Sicherheit --------------------------------------------------------------
|
||||
@@ -42,9 +44,9 @@ OIDC_CLIENT_SECRET=
|
||||
OIDC_SCOPES=openid profile email
|
||||
|
||||
# --- Logo-Service ------------------------------------------------------------
|
||||
# Verzeichnis für den lokalen Logo-Cache (im Container auf das Volume gemountet).
|
||||
# Logos werden ausschließlich von hier ausgeliefert – im Seitenaufruf entsteht
|
||||
# kein Zugriff auf fremde Dienste.
|
||||
# Verzeichnis für den lokalen Logo-Cache. Beim Betrieb mit Compose zeigt es fest
|
||||
# auf das Volume und muss hier nicht gesetzt werden. Logos werden ausschließlich
|
||||
# von dort ausgeliefert – im Seitenaufruf entsteht kein Zugriff nach außen.
|
||||
LOGO_STORAGE_DIR=/data/logos
|
||||
|
||||
# Der lokale simple-icons-Katalog deckt die meisten Marken ohne Netzzugriff ab.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Gitea Actions
|
||||
|
||||
Zwei Workflows:
|
||||
|
||||
| Datei | Auslöser | Was passiert |
|
||||
|---|---|---|
|
||||
| `ci.yml` | jeder Push, jeder Pull Request | Backend: `ruff check`, `ruff format --check`, `pytest` gegen einen PostgreSQL-Dienst. Frontend: `npm ci`, `tsc --noEmit`, `eslint`, `vitest`, Bundle-Bau. |
|
||||
| `build.yml` | Push auf `main`, Tags `v*` | Baut beide Images für `linux/amd64` und lädt sie in die Gitea-Registry. **Kein Deploy** – das Ausrollen erfolgt von Hand. |
|
||||
|
||||
## Benötigte Repo-Secrets
|
||||
|
||||
| Name | Zweck | Woher |
|
||||
|---|---|---|
|
||||
| `REGISTRY_TOKEN` | Anmeldung an `git.menzel.center` zum Hochladen der Images | Gitea → Benutzereinstellungen → Applications → **Generate New Token** mit dem Recht `write:package` |
|
||||
|
||||
Anlegen unter *Repository → Settings → Actions → Secrets → Add Secret*.
|
||||
|
||||
Als Benutzername verwendet der Workflow `${{ github.actor }}`, also den Auslöser
|
||||
des Laufs. Dieser Benutzer braucht Schreibrecht auf die Pakete des Namensraums
|
||||
`menzeljonas`.
|
||||
|
||||
## Erzeugte Tags
|
||||
|
||||
| Auslöser | Tags |
|
||||
|---|---|
|
||||
| Push auf `main` | `:main`, `:sha-<kurz>` |
|
||||
| Tag `v1.2.3` | `:1.2.3`, `:1.2`, `:latest` |
|
||||
|
||||
Zusätzlich legt jeder Lauf ein `:buildcache`-Manifest ab. Es dient
|
||||
ausschließlich dem Schichten-Cache und ist nicht zum Ausrollen gedacht.
|
||||
|
||||
## Voraussetzungen an den Runner
|
||||
|
||||
Der Runner muss Docker erreichen können (gemounteter Socket) und das Label
|
||||
`ubuntu-latest` anbieten. Die vollständige Einrichtung steht in
|
||||
[`../../docs/runner.md`](../../docs/runner.md).
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Images bauen
|
||||
|
||||
# Bei Push auf main und bei Versions-Tags. Ausgerollt wird nicht automatisch –
|
||||
# das Aktualisieren auf docker-srv001 passiert von Hand, siehe docs/deployment.md.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: git.menzel.center
|
||||
NAMESPACE: menzeljonas
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
component:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: An der Registry anmelden
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Tags und Beschriftungen ermitteln
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/moneyfy-${{ matrix.component }}
|
||||
# `latest` wird nur für Versions-Tags gesetzt, nicht für jeden main-Push.
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=sha,prefix=sha-,format=short,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=moneyfy-${{ matrix.component }}
|
||||
org.opencontainers.image.description=moneyfy – Planung monatlicher Kosten und Einkünfte
|
||||
org.opencontainers.image.licenses=MIT
|
||||
|
||||
- name: Bauen und hochladen
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./${{ matrix.component }}
|
||||
target: production
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Registry-Cache statt type=gha: der Gitea-Runner bringt keinen
|
||||
# Actions-Cache-Dienst mit.
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.NAMESPACE }}/moneyfy-${{ matrix.component }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.NAMESPACE }}/moneyfy-${{ matrix.component }}:buildcache,mode=max
|
||||
provenance: false
|
||||
|
||||
- name: Ergebnis zusammenfassen
|
||||
run: |
|
||||
echo "### moneyfy-${{ matrix.component }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,89 @@
|
||||
name: CI
|
||||
|
||||
# Läuft bei jedem Push und jedem Pull Request, unabhängig vom Branch.
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_DB: moneyfy_test
|
||||
POSTGRES_USER: moneyfy
|
||||
POSTGRES_PASSWORD: moneyfy
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U moneyfy -d moneyfy_test"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
|
||||
env:
|
||||
# Die Tests lesen DATABASE_URL; der Dienst ist unter seinem Namen erreichbar.
|
||||
DATABASE_URL: postgresql+asyncpg://moneyfy:moneyfy@postgres:5432/moneyfy_test
|
||||
SECRET_KEY: ci-secret-key-mindestens-32-zeichen-lang
|
||||
ENVIRONMENT: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: uv installieren
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Abhängigkeiten installieren
|
||||
# uv lädt bei Bedarf auch den passenden Python-Interpreter.
|
||||
run: |
|
||||
uv venv --python 3.12 .venv
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
- name: ruff check
|
||||
run: .venv/bin/ruff check .
|
||||
|
||||
- name: ruff format
|
||||
run: .venv/bin/ruff format --check .
|
||||
|
||||
- name: pytest
|
||||
run: .venv/bin/python -m pytest -q
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Abhängigkeiten installieren
|
||||
run: npm ci --no-audit --no-fund
|
||||
|
||||
- name: Typen prüfen
|
||||
run: npm run typecheck
|
||||
|
||||
- name: eslint
|
||||
run: npm run lint
|
||||
|
||||
- name: vitest
|
||||
run: npm run test
|
||||
|
||||
- name: Bundle bauen
|
||||
run: npx vite build
|
||||
@@ -111,6 +111,21 @@ die Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
- Benachrichtigungen sind in den Einstellungen pflegbar, samt Einrichtungsstand
|
||||
der Kanäle und Versandprotokoll.
|
||||
|
||||
- Mehrstufige Dockerfiles für Backend und Frontend, beide mit einer Stufe für
|
||||
die Entwicklung und einer für den Betrieb; beide Images laufen unprivilegiert.
|
||||
- Entrypoint des Backends wartet auf die Datenbank, spielt die Migrationen ein
|
||||
und legt die Stammdaten nur beim leeren Kategoriebaum an.
|
||||
- `docker-compose.yml` mit Netz-Trennung (die Datenbank ist ausschließlich
|
||||
intern erreichbar), Healthchecks und benannten Volumes für Datenbank und
|
||||
Logo-Cache.
|
||||
- nginx liefert das Bundle aus und reicht `/api` weiter; `index.html` bleibt
|
||||
ungecacht, die gehashten Assets ein Jahr.
|
||||
- Gitea-Workflows `ci.yml` (Lint, Tests, Bundle-Bau) und `build.yml` (Images für
|
||||
linux/amd64 in die Gitea-Registry, ohne Deploy-Schritt).
|
||||
- `docs/runner.md` zur Einrichtung des act_runners samt Fehlersuche und
|
||||
`docs/deployment.md` zu NPMplus, Aktualisierung und Sicherung.
|
||||
- `python -m app.scripts.seed --if-empty` seedt nur bei leerem Kategoriebaum.
|
||||
|
||||
### Geändert
|
||||
|
||||
- `SECRET_KEY` muss mindestens 32 Zeichen lang sein (Vorgabe von HS256); in
|
||||
|
||||
@@ -38,6 +38,28 @@ _Platzhalter – werden ergänzt, sobald die Oberfläche steht._
|
||||
| Charts | Recharts |
|
||||
| Tests | pytest + httpx, vitest + testing-library |
|
||||
|
||||
## Schnellstart (Betrieb)
|
||||
|
||||
Voraussetzungen: Docker mit Compose-Plugin und ein externes Netz `proxy`.
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/moneyfy && cd /opt/moneyfy
|
||||
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
|
||||
|
||||
# Mindestens setzen: POSTGRES_PASSWORD, SECRET_KEY, MONEYFY_ADMIN_PASSWORD
|
||||
$EDITOR .env
|
||||
|
||||
docker login git.menzel.center
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Das Backend wartet auf die Datenbank, spielt die Migrationen ein und legt beim
|
||||
allerersten Start den Kategoriebaum an. Danach läuft die Oberfläche auf
|
||||
`http://127.0.0.1:8087`. Die vollständige Einrichtung samt Reverse Proxy und
|
||||
Sicherung steht in [docs/deployment.md](docs/deployment.md).
|
||||
|
||||
## Schnellstart (lokale Entwicklung)
|
||||
|
||||
Voraussetzungen: Python 3.12, [uv](https://docs.astral.sh/uv/), Node 20+,
|
||||
@@ -146,6 +168,29 @@ docs/ Betriebs- und Runner-Dokumentation
|
||||
.gitea/ Gitea-Actions-Workflows
|
||||
```
|
||||
|
||||
Alternativ lässt sich alles aus dem Quellcode bauen:
|
||||
|
||||
```bash
|
||||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Die Override-Datei baut beide Images lokal, bindet den Quellcode ein und öffnet
|
||||
die Ports für Backend (8000) und Vite (5173).
|
||||
|
||||
## Bauen und Ausrollen
|
||||
|
||||
Zwei Gitea-Workflows unter `.gitea/workflows/`:
|
||||
|
||||
- **`ci.yml`** läuft bei jedem Push und Pull Request: `ruff`, `pytest`,
|
||||
`tsc --noEmit`, `eslint`, `vitest` und der Bundle-Bau.
|
||||
- **`build.yml`** läuft auf `main` und bei Tags `v*` und lädt beide Images für
|
||||
`linux/amd64` in die Gitea-Registry. Push auf `main` erzeugt `:main` und
|
||||
`:sha-<kurz>`, ein Tag `v1.2.3` zusätzlich `:1.2.3`, `:1.2` und `:latest`.
|
||||
|
||||
Ausgerollt wird **nicht** automatisch. Die Einrichtung des Runners beschreibt
|
||||
[docs/runner.md](docs/runner.md), das Ausrollen [docs/deployment.md](docs/deployment.md).
|
||||
|
||||
## Lizenz
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
*.egg-info/
|
||||
.env
|
||||
.data/
|
||||
|
||||
# Tests und Skripte bleiben verfügbar: das Entwicklungs-Image kopiert den ganzen
|
||||
# Baum, das Produktions-Image nur app/, alembic/ und den Entrypoint.
|
||||
@@ -0,0 +1,79 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Backend-Image für moneyfy. Mehrstufig, damit im Endbild weder Build-Werkzeuge
|
||||
# noch Entwicklungsabhängigkeiten landen.
|
||||
|
||||
# --- Abhängigkeiten -----------------------------------------------------------
|
||||
FROM python:3.12-slim-bookworm AS deps
|
||||
|
||||
# uv installiert die Abhängigkeiten deutlich schneller als pip.
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /bin/uv
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
UV_LINK_MODE=copy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Nur die Abhängigkeitsdeklaration kopieren – die Schicht bleibt gültig,
|
||||
# solange sich pyproject.toml nicht ändert.
|
||||
COPY pyproject.toml ./
|
||||
RUN /bin/uv pip install --system --no-cache -r pyproject.toml
|
||||
|
||||
# --- Laufzeitbasis ------------------------------------------------------------
|
||||
FROM python:3.12-slim-bookworm AS runtime
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONPATH=/app \
|
||||
TZ=Europe/Berlin \
|
||||
LOGO_STORAGE_DIR=/data/logos
|
||||
|
||||
# tzdata wird für Europe/Berlin gebraucht, curl für den Healthcheck.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends tzdata curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --system --gid 10001 moneyfy \
|
||||
&& useradd --system --uid 10001 --gid moneyfy --create-home --home-dir /home/moneyfy moneyfy
|
||||
|
||||
COPY --from=deps /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||
COPY --from=deps /usr/local/bin /usr/local/bin
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# --- Entwicklung --------------------------------------------------------------
|
||||
# Wird von docker-compose.override.yml genutzt; der Quellcode kommt per Volume.
|
||||
FROM runtime AS development
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /usr/local/bin/uv
|
||||
COPY pyproject.toml ./
|
||||
RUN uv pip install --system --no-cache -r pyproject.toml --extra dev
|
||||
|
||||
COPY --chown=moneyfy:moneyfy . .
|
||||
RUN mkdir -p /data/logos && chown -R moneyfy:moneyfy /data
|
||||
|
||||
USER moneyfy
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
|
||||
# --- Produktion ---------------------------------------------------------------
|
||||
FROM runtime AS production
|
||||
|
||||
COPY --chown=moneyfy:moneyfy alembic.ini pyproject.toml ./
|
||||
COPY --chown=moneyfy:moneyfy alembic ./alembic
|
||||
COPY --chown=moneyfy:moneyfy app ./app
|
||||
COPY --chown=moneyfy:moneyfy docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh \
|
||||
&& mkdir -p /data/logos \
|
||||
&& chown -R moneyfy:moneyfy /data
|
||||
|
||||
USER moneyfy
|
||||
EXPOSE 8000
|
||||
VOLUME ["/data"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS http://127.0.0.1:8000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"]
|
||||
@@ -1,14 +1,28 @@
|
||||
"""CLI: Stammdaten seeden – `python -m app.scripts.seed`."""
|
||||
"""CLI: Stammdaten seeden – `python -m app.scripts.seed [--if-empty]`."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.models import Category
|
||||
from app.services.seed import seed_all
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async def main(only_if_empty: bool) -> None:
|
||||
async with SessionLocal() as session:
|
||||
if only_if_empty:
|
||||
vorhanden = (
|
||||
await session.execute(select(func.count()).select_from(Category))
|
||||
).scalar_one()
|
||||
if vorhanden:
|
||||
print(f"Seed übersprungen: es gibt bereits {vorhanden} Kategorien.")
|
||||
await engine.dispose()
|
||||
return
|
||||
|
||||
result = await seed_all(session)
|
||||
|
||||
await engine.dispose()
|
||||
print(
|
||||
f"Seed fertig: {result['categories']} Kategorien, "
|
||||
@@ -17,4 +31,12 @@ async def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--if-empty",
|
||||
action="store_true",
|
||||
help="Nur seeden, wenn noch keine Kategorie existiert. Für den Containerstart gedacht, "
|
||||
"damit bewusst gelöschte Kategorien nicht wiederkehren.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(args.if_empty))
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# Startskript des Backend-Containers.
|
||||
#
|
||||
# Wartet auf die Datenbank, bringt das Schema auf den aktuellen Stand und legt
|
||||
# beim allerersten Start die Stammdaten an. Erst danach startet die Anwendung.
|
||||
set -eu
|
||||
|
||||
WARTEZEIT_SEKUNDEN="${DB_WAIT_SECONDS:-60}"
|
||||
|
||||
warte_auf_datenbank() {
|
||||
i=0
|
||||
while [ "$i" -lt "$WARTEZEIT_SEKUNDEN" ]; do
|
||||
if python -c "
|
||||
import asyncio, sys
|
||||
from sqlalchemy import text
|
||||
from app.db.session import engine
|
||||
|
||||
async def pruefen():
|
||||
async with engine.connect() as verbindung:
|
||||
await verbindung.execute(text('SELECT 1'))
|
||||
await engine.dispose()
|
||||
|
||||
try:
|
||||
asyncio.run(pruefen())
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
i=$((i + 1))
|
||||
[ "$i" -eq 1 ] && echo "Warte auf die Datenbank …"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Die Datenbank war nach ${WARTEZEIT_SEKUNDEN} Sekunden nicht erreichbar." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
warte_auf_datenbank
|
||||
|
||||
echo "Migrationen werden angewendet …"
|
||||
alembic upgrade head
|
||||
|
||||
# Idempotent und nur beim leeren Kategoriebaum – bewusst gelöschte Kategorien
|
||||
# kehren dadurch nicht bei jedem Neustart zurück.
|
||||
python -m app.scripts.seed --if-empty
|
||||
|
||||
echo "moneyfy-Backend startet."
|
||||
exec "$@"
|
||||
@@ -1,8 +1,11 @@
|
||||
# Lokale Entwicklung: Hot Reload und offene Ports.
|
||||
# Lokale Entwicklung: aus dem Quellcode bauen, Hot Reload, offene Ports.
|
||||
# Kopieren nach `docker-compose.override.yml` – die Datei wird nicht eingecheckt.
|
||||
#
|
||||
# Compose liest sie automatisch zusätzlich zu docker-compose.yml.
|
||||
|
||||
services:
|
||||
moneyfy-db:
|
||||
# Datenbank direkt erreichbar, z. B. für psql oder einen SQL-Client.
|
||||
# Datenbank direkt erreichbar, etwa für psql oder einen SQL-Client.
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
|
||||
@@ -16,7 +19,9 @@ services:
|
||||
ENVIRONMENT: development
|
||||
DEBUG: "true"
|
||||
COOKIE_SECURE: "false"
|
||||
# Im Entwicklungsbetrieb sollen keine Mails hinausgehen.
|
||||
SCHEDULER_ENABLED: "false"
|
||||
# Der Vite-Server läuft auf einem anderen Port und braucht daher CORS.
|
||||
CORS_ORIGINS: http://localhost:5173
|
||||
volumes:
|
||||
# Quellcode einblenden, damit --reload greift.
|
||||
@@ -32,10 +37,16 @@ services:
|
||||
image: moneyfy-frontend:dev
|
||||
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||
environment:
|
||||
VITE_API_BASE_URL: http://localhost:8000
|
||||
# Vite reicht /api an das Backend weiter, siehe vite.config.ts.
|
||||
VITE_API_PROXY: http://moneyfy-backend:8000
|
||||
volumes:
|
||||
- ./frontend/src:/app/src
|
||||
- ./frontend/public:/app/public
|
||||
- ./frontend/index.html:/app/index.html:ro
|
||||
- ./frontend/vite.config.ts:/app/vite.config.ts:ro
|
||||
- ./frontend/tailwind.config.js:/app/tailwind.config.js:ro
|
||||
ports:
|
||||
- "127.0.0.1:5173:5173"
|
||||
# Der Entwicklungsserver kennt /healthz nicht.
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Betrieb von moneyfy mit Docker Compose.
|
||||
#
|
||||
# Vor dem ersten Start: `cp .env.example .env` und die Datei ausfüllen.
|
||||
# Anschließend genügt `docker compose up -d`; das Backend legt beim Start das
|
||||
# Schema an und seedt die Stammdaten.
|
||||
|
||||
name: moneyfy
|
||||
|
||||
services:
|
||||
moneyfy-db:
|
||||
image: postgres:17-alpine
|
||||
container_name: moneyfy-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-moneyfy}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-moneyfy}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD muss in .env gesetzt sein}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||
TZ: ${TIMEZONE:-Europe/Berlin}
|
||||
volumes:
|
||||
- moneyfy-pgdata:/var/lib/postgresql/data
|
||||
# Die Datenbank ist ausschließlich im internen Netz erreichbar.
|
||||
networks:
|
||||
- moneyfy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-moneyfy} -d ${POSTGRES_DB:-moneyfy}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
moneyfy-backend:
|
||||
image: git.menzel.center/menzeljonas/moneyfy-backend:${TAG:-latest}
|
||||
container_name: moneyfy-backend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
moneyfy-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-moneyfy}:${POSTGRES_PASSWORD}@moneyfy-db:5432/${POSTGRES_DB:-moneyfy}
|
||||
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY muss in .env gesetzt sein, mindestens 32 Zeichen}
|
||||
ENVIRONMENT: ${ENVIRONMENT:-production}
|
||||
DEBUG: ${DEBUG:-false}
|
||||
TIMEZONE: ${TIMEZONE:-Europe/Berlin}
|
||||
HOLIDAY_REGION: ${HOLIDAY_REGION:-DE-NW}
|
||||
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:8087}
|
||||
|
||||
MONEYFY_ADMIN_USER: ${MONEYFY_ADMIN_USER:-admin}
|
||||
MONEYFY_ADMIN_PASSWORD: ${MONEYFY_ADMIN_PASSWORD:-}
|
||||
ACCESS_TOKEN_TTL_MINUTES: ${ACCESS_TOKEN_TTL_MINUTES:-30}
|
||||
REFRESH_TOKEN_TTL_DAYS: ${REFRESH_TOKEN_TTL_DAYS:-14}
|
||||
COOKIE_SECURE: ${COOKIE_SECURE:-true}
|
||||
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
|
||||
|
||||
LOGO_STORAGE_DIR: /data/logos
|
||||
LOGODEV_API_KEY: ${LOGODEV_API_KEY:-}
|
||||
BRANDFETCH_API_KEY: ${BRANDFETCH_API_KEY:-}
|
||||
LOGO_AUTO_RESOLVE: ${LOGO_AUTO_RESOLVE:-true}
|
||||
|
||||
NOTIFICATIONS_ENABLED: ${NOTIFICATIONS_ENABLED:-true}
|
||||
SCHEDULER_ENABLED: ${SCHEDULER_ENABLED:-true}
|
||||
NOTIFICATION_HOUR: ${NOTIFICATION_HOUR:-7}
|
||||
NOTIFICATION_MINUTE: ${NOTIFICATION_MINUTE:-0}
|
||||
SMTP_HOST: ${SMTP_HOST:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_USER: ${SMTP_USER:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_FROM: ${SMTP_FROM:-}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||
SMTP_USE_SSL: ${SMTP_USE_SSL:-false}
|
||||
APPRISE_URLS: ${APPRISE_URLS:-}
|
||||
volumes:
|
||||
# Logo-Cache; die Dateien überleben ein Update des Images.
|
||||
- moneyfy-data:/data
|
||||
networks:
|
||||
- moneyfy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
moneyfy-frontend:
|
||||
image: git.menzel.center/menzeljonas/moneyfy-frontend:${TAG:-latest}
|
||||
container_name: moneyfy-frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
moneyfy-backend:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
# Nur lokal gebunden – von außen kommt man ausschließlich über NPMplus.
|
||||
- "127.0.0.1:8087:8080"
|
||||
networks:
|
||||
# `moneyfy` für den Weg zum Backend, `proxy` für NPMplus.
|
||||
- moneyfy
|
||||
- proxy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
networks:
|
||||
moneyfy:
|
||||
driver: bridge
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
moneyfy-pgdata:
|
||||
name: moneyfy-pgdata
|
||||
moneyfy-data:
|
||||
name: moneyfy-data
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
.env.local
|
||||
@@ -0,0 +1,47 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Frontend-Image für moneyfy: Vite-Bundle bauen, mit nginx ausliefern.
|
||||
|
||||
# --- Abhängigkeiten -----------------------------------------------------------
|
||||
FROM node:22-bookworm-slim AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Nur die Sperrdatei kopieren – die Schicht bleibt gültig, solange sich die
|
||||
# Abhängigkeiten nicht ändern.
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
# --- Entwicklung --------------------------------------------------------------
|
||||
# Wird von docker-compose.override.yml genutzt; der Quellcode kommt per Volume.
|
||||
FROM deps AS development
|
||||
|
||||
ENV NODE_ENV=development
|
||||
COPY . .
|
||||
|
||||
# Vite legt seinen Cache unter node_modules/.vite ab – der Benutzer braucht Schreibrecht.
|
||||
RUN chown -R node:node /app
|
||||
|
||||
USER node
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||
|
||||
# --- Bau ----------------------------------------------------------------------
|
||||
FROM deps AS build
|
||||
|
||||
COPY . .
|
||||
|
||||
# `npm run build` prüft zuerst die Typen und bricht bei Fehlern ab.
|
||||
RUN npm run build
|
||||
|
||||
# --- Auslieferung -------------------------------------------------------------
|
||||
# Das unprivilegierte Image läuft als Benutzer nginx und lauscht auf 8080.
|
||||
FROM nginxinc/nginx-unprivileged:1.27-alpine AS production
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1:8080/healthz >/dev/null || exit 1
|
||||
@@ -0,0 +1,82 @@
|
||||
# nginx-Konfiguration des Frontend-Containers.
|
||||
#
|
||||
# Liefert das gebaute Vite-Bundle aus und reicht /api an das Backend weiter.
|
||||
# Das Image läuft unprivilegiert, deshalb Port 8080 statt 80.
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Hinter dem Reverse Proxy kommen die echten Adressen aus den Kopfzeilen.
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
client_max_body_size 4m;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain text/css application/javascript application/json
|
||||
image/svg+xml application/xml;
|
||||
|
||||
# Die Anwendung wird ausschließlich hinter einem Reverse Proxy betrieben;
|
||||
# TLS und HSTS übernimmt dieser.
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||
|
||||
# --- API -----------------------------------------------------------------
|
||||
location /api/ {
|
||||
proxy_pass http://moneyfy-backend:8000;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Exporte können bei vielen Buchungen etwas dauern.
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
|
||||
# API-Antworten enthalten Kontostände – nichts davon zwischenspeichern.
|
||||
proxy_buffering off;
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
|
||||
# --- Statische Dateien ---------------------------------------------------
|
||||
# Vite hängt einen Inhaltshash an die Dateinamen; sie sind unveränderlich.
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /favicon.svg {
|
||||
expires 7d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
# Alles Übrige beantwortet die Single-Page-Anwendung.
|
||||
location / {
|
||||
# index.html darf nie im Cache hängen bleiben, sonst zeigt ein Browser
|
||||
# nach dem Update weiter auf die alten Bundle-Namen.
|
||||
add_header Cache-Control "no-cache" always;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user