diff --git a/notes/deploy.md b/notes/deploy.md index dcf68ef..1a976a6 100644 --- a/notes/deploy.md +++ b/notes/deploy.md @@ -1,5 +1,26 @@ # Deployment mit Podman +## Was muss auf den Server? + +Nur der Quellcode — das Image wird direkt auf dem Server gebaut. + +```sh +# Einmalig: Repo klonen +git clone https://codeberg.org//kver.git +cd kver +``` + +Oder bei bestehendem Checkout: + +```sh +git pull origin master +``` + +Bestehende Daten (DB + Uploads) liegen auf dem Server in Named Volumes und +bleiben bei Image-Updates unberührt — die müssen nicht mitübertragen werden. +Beim ersten Deployment: DB und Medien einmalig in die Volumes kopieren +(s. Abschnitt „Bestehende Daten übernehmen"). + ## Bauen ```sh @@ -14,67 +35,99 @@ Podman baut OCI-Images, dort gibt es kein eingebettetes HEALTHCHECK. Der Healthcheck kommt stattdessen aus der Quadlet-Unit (s.u.); alternativ `podman build --format=docker` oder `podman run --health-cmd=...`. -## Starten +## Starten (Port 7777) ```sh podman run -d --name kver \ - -p 127.0.0.1:8080:8080 \ + -p 127.0.0.1:7777:8080 \ -v kver-data:/app/data \ -v kver-media:/app/static/media \ kver ``` - `kver-data` hält die SQLite-DB (`/app/data/kver.db`), `kver-media` die - hochgeladenen Bilder. Beides Named Volumes -> überleben Image-Updates. + hochgeladenen Bilder. Beides Named Volumes → überleben Image-Updates. - Port nur an localhost binden; nach außen geht es über den Reverse-Proxy. +- Der Container lauscht intern immer auf `:8080`; der externe Port (hier 7777) + ist nur das Host-seitige Mapping. Konfiguration über Umgebungsvariablen: | Variable | Default | Bedeutung | |-------------|---------------------|----------------------| -| `KVER_ADDR` | `:8080` | Listen-Adresse | +| `KVER_ADDR` | `:8080` | Listen-Adresse im Container | | `KVER_DB` | `/app/data/kver.db` | Pfad zur SQLite-Datei | ## Bestehende Daten übernehmen -DB und Medien einmalig in die Volumes kopieren (Container muss dafür kurz -gelaufen sein bzw. die Volumes existieren): +DB und Medien einmalig in die Volumes kopieren (Volumes müssen existieren; +Container kann laufen oder gestoppt sein): ```sh podman volume create kver-data podman volume create kver-media -podman cp kver.db kver:/app/data/kver.db # bei laufendem Container, oder: -cp kver.db "$(podman volume inspect kver-data -f '{{.Mountpoint}}')/" -cp static/media/* "$(podman volume inspect kver-media -f '{{.Mountpoint}}')/" + +# Rootless: podman unshare sorgt dafür, dass die UID ins User-Namespace- +# Mapping passt — ohne das bekommt der Container die Dateien nicht zu lesen. +podman unshare cp kver.db \ + "$(podman volume inspect kver-data --format '{{.Mountpoint}}')/" +podman unshare cp -r static/media/. \ + "$(podman volume inspect kver-media --format '{{.Mountpoint}}')/" ``` -Danach Container neu starten. (Rootless: `podman unshare cp ...`, damit die -UID-Zuordnung ins User-Namespace-Mapping passt.) +Danach Container (neu) starten. Die App legt beim ersten Start alle fehlenden +Tabellen und Indizes automatisch per `CREATE ... IF NOT EXISTS` an. -## Reverse-Proxy (TLS) +## Reverse-Proxy (TLS) — Caddy -Der Container spricht nur HTTP. Davor gehört ein TLS-terminierender Proxy -(Caddy/nginx), der `X-Forwarded-Proto: https` setzt — daran erkennt die App -HTTPS und setzt das `Secure`-Flag auf den Session-Cookie (siehe `isHTTPS`). +Caddy ist die einfachste Wahl: automatisches TLS, setzt `X-Forwarded-Proto` +und `X-Forwarded-For` ohne Konfiguration, kein eigenes Body-Size-Problem. -nginx-Beispiel: +`/etc/caddy/Caddyfile` (oder `~/.config/caddy/Caddyfile` bei rootless): -```nginx -location / { - proxy_pass http://127.0.0.1:8080; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - client_max_body_size 20m; # Uploads bis 17 MB durchlassen +```caddyfile +kver.example.org { + # Unveränderliche Uploads cachen — Dateinamen sind einmalig (-), + # Browser lädt jede Datei genau einmal. + header /static/media/* Cache-Control "public, max-age=31536000, immutable" + + reverse_proxy 127.0.0.1:7777 } ``` -`client_max_body_size` muss über dem App-Limit (17 MB, `maxUploadBytes`) -liegen, sonst kappt nginx vor der App. +Das ist alles. Caddy kümmert sich um Let's-Encrypt-Zertifikat, HTTPS-Redirect +und setzt automatisch `X-Forwarded-Proto: https` → Session-Cookie bekommt das +`Secure`-Flag, Rate-Limit sieht echte Client-IPs. -Hinweis Rate-Limiting: `httprate.LimitByIP` auf den Auth-Routen sieht hinter -einem Proxy die Proxy-IP. Entweder den Proxy die echte Client-IP als -`X-Real-IP`/`X-Forwarded-For` setzen lassen und chi's `middleware.RealIP` -vorschalten — oder damit leben, dass das Limit global statt pro IP wirkt. +Optional Body-Limit vor der App (App begrenzt selbst auf 17 MB): + +```caddyfile + request_body { + max_size 20MB + } +``` + +### Warum /static/media/* nicht direkt aus Caddy serven? + +Der `nosniff`-Header auf Uploads ist sicherheitsrelevant (animierte GIFs werden +nicht re-kodiert; ohne `nosniff` könnte ein präpariertes GIF als HTML/JS +interpretiert werden → Stored XSS). Serviert Caddy die Dateien direkt, ist die +App-Middleware außen vor. Mit `reverse_proxy` bleibt die App der einzige +Auslieferer und der Header sitzt immer drauf. + +Außerdem liegt das Named Volume bei rootless Podman unter einem Subuid-Pfad, +den ein Systemdienst nicht ohne Weiteres lesen kann. + +### nginx (alternativ) + +```nginx +location / { + proxy_pass http://127.0.0.1:7777; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + client_max_body_size 20m; # muss über App-Limit (17 MB) liegen +} +``` ## Autostart mit systemd (Quadlet) @@ -86,11 +139,11 @@ Description=kver (Kontrollverlust) [Container] Image=localhost/kver:latest -PublishPort=127.0.0.1:8080:8080 +PublishPort=127.0.0.1:7777:8080 Volume=kver-data:/app/data Volume=kver-media:/app/static/media -# Healthcheck hier statt im Containerfile: Podman baut standardmäßig -# OCI-Images, die kennen kein HEALTHCHECK (daher die Warnung beim Build). +# Healthcheck hier statt im Containerfile: Podman baut OCI-Images, +# die kennen kein eingebettetes HEALTHCHECK. HealthCmd=wget -qO /dev/null http://localhost:8080/entry/feed/0 HealthInterval=30s @@ -109,11 +162,19 @@ systemctl --user start kver loginctl enable-linger "$USER" # damit der Dienst ohne Login läuft ``` +Status prüfen: + +```sh +systemctl --user status kver +podman healthcheck run kver +``` + ## Update einspielen ```sh +git pull origin master podman build -t kver . -systemctl --user restart kver # bzw. podman stop kver && podman run ... +systemctl --user restart kver ``` Die App fährt bei SIGTERM sauber herunter (laufende Requests werden zu Ende