WEB-2: Self-hosted public site (publicsite-nginx + POST /api/publish, atomic swap)

POST /api/publish: rendert SiteSnapshot->HTML in _staging_new/, atomic swap ->
live/ (rename, ein Syscall). publicsite-nginx:alpine serviert live/ read-only
auf Port 8081. Shared Volume api(rw)/publicsite(ro). Manager bleibt LAN-only.
5 neue Tests (atomic swap, UTF-8, mehrfach), 184/184 gruen. compose config OK.
Vhost-Snippet + web-deploy.md (Deutsch) beigelegt; <DOMAIN> wartet auf Julian.
This commit is contained in:
2026-06-07 01:44:28 +02:00
parent 04971bf3bb
commit c0ecf2022d
9 changed files with 401 additions and 0 deletions

View File

@@ -21,6 +21,11 @@ KEYS_PATH=/mnt/SSD/gerbil/keys
# Backup-Rotation: Anzahl Tage (Standard: 7)
BACKUP_KEEP_DAYS=7
# WEB-2: Oeffentliche Webseite (Shared Volume: api schreibt, publicsite-nginx liest)
PUBLICSITE_PATH=/mnt/JailStorage/DockerVolumes/gerbilmanager/publicsite
# Port fuer den publicsite-nginx (Julian's externer nginx leitet darauf weiter)
PUBLICSITE_PORT=8081
# KI-Funktionen (Verkaufstext + Posteingang-Entwurf)
# Beliebiger OpenAI-kompatibler Anbieter — Optionen in docs/ai-provider.md
# Leer lassen = KI deaktiviert (kein Fehler, nur 503 AiKeyMissing)

View File

@@ -48,9 +48,12 @@ services:
AI__BaseUrl: "${AI__BaseUrl:-}"
AI__ApiKey: "${AI__ApiKey:-}"
AI__Model: "${AI__Model:-gemini-flash-latest}"
# WEB-2: Pfad wo POST /api/publish die oeffentliche Seite hinschreibt
PublicSite__RootPath: /data/publicsite
volumes:
- photos:/data/photos
- keys:/data/keys
- publicsite:/data/publicsite
depends_on:
db:
condition: service_healthy
@@ -74,6 +77,21 @@ services:
api:
condition: service_healthy
# --- nginx Public Site (WEB-2) ---
# Serviert NUR die statische oeffentliche Seite (live/ aus dem publicsite-Volume).
# SICHERHEIT: Kein Proxy auf api/frontend — nur statisches HTML nach aussen.
# Julian's externer nginx-Proxy leitet <DOMAIN> auf Port 8081 weiter.
publicsite:
image: nginx:alpine
restart: unless-stopped
ports:
- "${PUBLICSITE_PORT:-8081}:80"
volumes:
- publicsite:/usr/share/nginx/html:ro
- ./nginx/publicsite.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
# --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) ---
backup:
image: postgres:17-alpine
@@ -122,3 +140,10 @@ volumes:
type: none
o: bind
device: "${BACKUPS_PATH:-/mnt/gerbil/backups}"
# WEB-2: gemeinsames Volume fuer api (rw) und publicsite-nginx (ro).
publicsite:
driver: local
driver_opts:
type: none
o: bind
device: "${PUBLICSITE_PATH:-/mnt/JailStorage/DockerVolumes/gerbilmanager/publicsite}"

View File

@@ -0,0 +1,29 @@
# GerbilManager — publicsite nginx (WEB-2)
# Serviert die statische oeffentliche Seite aus dem live/-Verzeichnis des Shared Volumes.
# SICHERHEIT: Kein Proxy auf die API, kein Zugriff auf den Manager.
server {
listen 80;
root /usr/share/nginx/html/live;
index index.html;
charset utf-8;
# Alle Seiten: no-cache (Aenderungen sofort sichtbar nach Veroeffentlichen)
location / {
try_files $uri $uri/index.html =404;
add_header Cache-Control "no-cache, must-revalidate";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
}
# CSS/Bilder: kurze TTL (1 Tag)
location ~* \.(css|png|jpg|jpeg|gif|ico|webp|svg)$ {
try_files $uri =404;
expires 1d;
add_header Cache-Control "public, max-age=86400";
}
# Kein Zugriff auf Staging-Verzeichnisse
location ~ ^/_(staging_new|old)/ {
return 403;
}
}

View File

@@ -0,0 +1,37 @@
# GerbilManager — Externer nginx-Vhost fuer die oeffentliche Webseite (WEB-2)
# In Julians bestehenden nginx-Reverse-Proxy einfuegen.
# <DOMAIN> ersetzen sobald der Hostname feststeht (Julian liefert ihn).
#
# SICHERHEIT: Dieser Vhost zeigt NUR auf den publicsite-Container (Port 8081).
# Der Manager (API + Frontend, Port 80) ist NICHT erreichbar von aussen —
# er hat keine Authentifizierung und muss LAN-only bleiben.
server {
listen 80;
server_name <DOMAIN>;
location / {
proxy_pass http://127.0.0.1:8081;
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 $scheme;
# Kein Buffering fuer kleine statische HTML-Seiten
proxy_buffering off;
}
}
# Fuer HTTPS (empfohlen, z.B. per Let's Encrypt via certbot):
# server {
# listen 443 ssl;
# server_name <DOMAIN>;
# ssl_certificate /etc/letsencrypt/live/<DOMAIN>/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/<DOMAIN>/privkey.pem;
# location / {
# proxy_pass http://127.0.0.1:8081;
# 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 $scheme;
# }
# }