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_DB=moneyfy
|
||||||
POSTGRES_USER=moneyfy
|
POSTGRES_USER=moneyfy
|
||||||
POSTGRES_PASSWORD=bitte-aendern
|
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
|
DATABASE_URL=postgresql+asyncpg://moneyfy:bitte-aendern@moneyfy-db:5432/moneyfy
|
||||||
|
|
||||||
# --- Sicherheit --------------------------------------------------------------
|
# --- Sicherheit --------------------------------------------------------------
|
||||||
@@ -42,9 +44,9 @@ OIDC_CLIENT_SECRET=
|
|||||||
OIDC_SCOPES=openid profile email
|
OIDC_SCOPES=openid profile email
|
||||||
|
|
||||||
# --- Logo-Service ------------------------------------------------------------
|
# --- Logo-Service ------------------------------------------------------------
|
||||||
# Verzeichnis für den lokalen Logo-Cache (im Container auf das Volume gemountet).
|
# Verzeichnis für den lokalen Logo-Cache. Beim Betrieb mit Compose zeigt es fest
|
||||||
# Logos werden ausschließlich von hier ausgeliefert – im Seitenaufruf entsteht
|
# auf das Volume und muss hier nicht gesetzt werden. Logos werden ausschließlich
|
||||||
# kein Zugriff auf fremde Dienste.
|
# von dort ausgeliefert – im Seitenaufruf entsteht kein Zugriff nach außen.
|
||||||
LOGO_STORAGE_DIR=/data/logos
|
LOGO_STORAGE_DIR=/data/logos
|
||||||
|
|
||||||
# Der lokale simple-icons-Katalog deckt die meisten Marken ohne Netzzugriff ab.
|
# 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
|
- Benachrichtigungen sind in den Einstellungen pflegbar, samt Einrichtungsstand
|
||||||
der Kanäle und Versandprotokoll.
|
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
|
### Geändert
|
||||||
|
|
||||||
- `SECRET_KEY` muss mindestens 32 Zeichen lang sein (Vorgabe von HS256); in
|
- `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 |
|
| Charts | Recharts |
|
||||||
| Tests | pytest + httpx, vitest + testing-library |
|
| 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)
|
## Schnellstart (lokale Entwicklung)
|
||||||
|
|
||||||
Voraussetzungen: Python 3.12, [uv](https://docs.astral.sh/uv/), Node 20+,
|
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
|
.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
|
## Lizenz
|
||||||
|
|
||||||
[MIT](LICENSE)
|
[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
|
import asyncio
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from app.db.session import SessionLocal, engine
|
from app.db.session import SessionLocal, engine
|
||||||
|
from app.models import Category
|
||||||
from app.services.seed import seed_all
|
from app.services.seed import seed_all
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main(only_if_empty: bool) -> None:
|
||||||
async with SessionLocal() as session:
|
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)
|
result = await seed_all(session)
|
||||||
|
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
print(
|
print(
|
||||||
f"Seed fertig: {result['categories']} Kategorien, "
|
f"Seed fertig: {result['categories']} Kategorien, "
|
||||||
@@ -17,4 +31,12 @@ async def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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.
|
# Kopieren nach `docker-compose.override.yml` – die Datei wird nicht eingecheckt.
|
||||||
|
#
|
||||||
|
# Compose liest sie automatisch zusätzlich zu docker-compose.yml.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
moneyfy-db:
|
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:
|
ports:
|
||||||
- "127.0.0.1:5432:5432"
|
- "127.0.0.1:5432:5432"
|
||||||
|
|
||||||
@@ -16,7 +19,9 @@ services:
|
|||||||
ENVIRONMENT: development
|
ENVIRONMENT: development
|
||||||
DEBUG: "true"
|
DEBUG: "true"
|
||||||
COOKIE_SECURE: "false"
|
COOKIE_SECURE: "false"
|
||||||
|
# Im Entwicklungsbetrieb sollen keine Mails hinausgehen.
|
||||||
SCHEDULER_ENABLED: "false"
|
SCHEDULER_ENABLED: "false"
|
||||||
|
# Der Vite-Server läuft auf einem anderen Port und braucht daher CORS.
|
||||||
CORS_ORIGINS: http://localhost:5173
|
CORS_ORIGINS: http://localhost:5173
|
||||||
volumes:
|
volumes:
|
||||||
# Quellcode einblenden, damit --reload greift.
|
# Quellcode einblenden, damit --reload greift.
|
||||||
@@ -32,10 +37,16 @@ services:
|
|||||||
image: moneyfy-frontend:dev
|
image: moneyfy-frontend:dev
|
||||||
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||||
environment:
|
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:
|
volumes:
|
||||||
- ./frontend/src:/app/src
|
- ./frontend/src:/app/src
|
||||||
- ./frontend/public:/app/public
|
- ./frontend/public:/app/public
|
||||||
- ./frontend/index.html:/app/index.html:ro
|
- ./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:
|
ports:
|
||||||
- "127.0.0.1:5173:5173"
|
- "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