build: Docker-Images, Compose-Setup und Gitea-Workflows
- Mehrstufige Dockerfiles je Dienst mit Stufen für Entwicklung und Betrieb, beide Images laufen als unprivilegierter Benutzer - Backend-Entrypoint wartet auf die Datenbank, migriert und seedt nur bei leerem Kategoriebaum (neues Flag --if-empty) - docker-compose.yml mit Netz-Trennung, Healthchecks und benannten Volumes; die Datenbank ist ausschließlich im internen Netz erreichbar - nginx liefert das Bundle aus und reicht /api weiter; index.html ungecacht, gehashte Assets ein Jahr - Gitea-Workflows: ci.yml für Lint, Tests und Bundle-Bau, build.yml für die Images nach linux/amd64 ohne Deploy-Schritt - docs/runner.md und docs/deployment.md, .dockerignore je Dienst Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,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 "$@"
|
||||
Reference in New Issue
Block a user