build: Docker-Images, Compose-Setup und Gitea-Workflows

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
moneyfy
2026-09-09 17:14:44 +02:00
co-authored by Claude Opus 5
parent 54c59c9f71
commit 87bee9b72a
17 changed files with 1184 additions and 10 deletions
+25 -3
View File
@@ -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))