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,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