Files
Kontrollverlust/notes/deploy.md
T
irrlichtandClaude Opus 5.5 030a5bd943 Moderation entfernt, JSON-API unter /api
- Moderationsseite und Melde-Funktion (report) komplett ausgebaut; folgt
  als eigenständiges Projekt
- alle API-Endpunkte unter /api, dort ausschließlich JSON: auch 404, 405,
  Rate-Limit (429) und Panics (500)
- Fehler nur über HTTP-Status; stille DB-Fehler in stats, logout und
  Vote-Zählern liefern jetzt 500 statt Nullen
- /auth/headerbar entfernt (Frontend nutzt /api/user/info)
- Frontend auf /api und statusbasierte Auswertung umgestellt
- notes/api.md neu als Referenz der aktuellen API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
2026-09-26 23:10:02 +02:00

234 lines
7.0 KiB
Markdown

# 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/<user>/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
podman build -t kver .
```
Multi-Stage-Build (`Containerfile`): statisches Go-Binary (CGO aus), Laufzeit-
Image ist Alpine mit unprivilegiertem Nutzer.
Die Warnung `HEALTHCHECK is not supported for OCI image format` ist erwartbar:
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 (Port 7777)
```sh
podman run -d --name kver \
-p 127.0.0.1:7777:8080 \
--env-file /etc/kver/kver.env \
-v kver-data:/app/data \
-v kver-media:/app/static/media \
kver
```
- Die Daten liegen in Postgres (`KVER_DSN`). `kver-data` hält nur noch die
GeoLite2-ASN-DB (und die alte SQLite-Datei), `kver-media` die 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 im Container |
| `KVER_DSN` | _(Pflicht)_ | Postgres-Verbindung, z. B. `postgres://kver:pw@host:5432/kontrollverlust?sslmode=disable` |
| `KVER_GEOIP_ASN` | _(leer)_ | Pfad zur GeoLite2-ASN-`.mmdb`; leer = ASN-Auflösung aus |
## GeoLite2-ASN-Datenbank (optional)
Für die netzwerktopologische Einordnung der Impressions (Spalte `impression.asn`).
Ohne diese DB läuft alles normal weiter, die Impressions haben dann nur kein ASN.
Die `GeoLite2-ASN.mmdb` gibt es kostenlos bei MaxMind (Konto + Lizenzschlüssel,
monatliche Updates). Datei ins Daten-Volume legen und den Pfad setzen:
```sh
podman unshare cp GeoLite2-ASN.mmdb \
"$(podman volume inspect kver-data --format '{{.Mountpoint}}')/"
```
Dann `KVER_GEOIP_ASN=/app/data/GeoLite2-ASN.mmdb` setzen. Die Datei wird beim Start
einmalig gemappt; für aktualisierte Daten die Datei ersetzen und den Dienst neu
starten.
## Postgres
Die App erwartet eine Datenbank, die ihrem Login-User gehört. Einmalig als
Admin:
```sql
CREATE ROLE kver LOGIN PASSWORD '...';
CREATE DATABASE kontrollverlust OWNER kver;
REVOKE CONNECT, TEMPORARY ON DATABASE kontrollverlust FROM PUBLIC;
```
Tabellen und Indizes legt die App beim Start selbst an (`CREATE ... IF NOT
EXISTS`, als `kver` → gehören `kver`). Nicht als Admin anlegen.
## Umzug SQLite → Postgres (einmalig)
Dienst stoppen, dann mit dem neuen Image die alte Datei aus dem Volume
importieren. Der Import läuft in einer Transaktion, prüft die Zeilenzahlen und
bricht ab, wenn in Postgres schon Daten liegen:
```sh
systemctl --user stop kver
podman run --rm --env-file /etc/kver/kver.env \
-v kver-data:/app/data:ro \
kver import-sqlite /app/data/kver.db
systemctl --user start kver
```
Der Container braucht dabei (und im Betrieb) dieselbe Netzwerkanbindung an
Postgres wie der Dienst selbst.
## Bestehende Medien übernehmen
Medien einmalig ins Volume kopieren (Volumes müssen existieren;
Container kann laufen oder gestoppt sein):
```sh
podman volume create kver-data
podman volume create kver-media
# 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 -r static/media/. \
"$(podman volume inspect kver-media --format '{{.Mountpoint}}')/"
```
Danach Container (neu) starten.
## Reverse-Proxy (TLS) — Caddy
Caddy ist die einfachste Wahl: automatisches TLS, setzt `X-Forwarded-Proto`
und `X-Forwarded-For` ohne Konfiguration, kein eigenes Body-Size-Problem.
`/etc/caddy/Caddyfile` (oder `~/.config/caddy/Caddyfile` bei rootless):
```caddyfile
kver.example.org {
# Unveränderliche Uploads cachen — Dateinamen sind einmalig (<unix>-<rand>),
# Browser lädt jede Datei genau einmal.
header /static/media/* Cache-Control "public, max-age=31536000, immutable"
reverse_proxy 127.0.0.1:7777
}
```
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.
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)
`~/.config/containers/systemd/kver.container` (rootless):
```ini
[Unit]
Description=kver (Kontrollverlust)
[Container]
Image=localhost/kver:latest
PublishPort=127.0.0.1:7777:8080
Volume=kver-data:/app/data
Volume=kver-media:/app/static/media
# Optional: ASN-Auflösung der Impressions. Pfad zeigt ins kver-data-Volume,
# daher zur Laufzeit setzen (nicht ins Image backen). Zeile weglassen, wenn
# keine GeoLite2-ASN.mmdb vorhanden ist -> ASN-Auflösung bleibt still aus.
Environment=KVER_GEOIP_ASN=/app/data/GeoLite2-ASN.mmdb
# Healthcheck hier statt im Containerfile: Podman baut OCI-Images,
# die kennen kein eingebettetes HEALTHCHECK.
HealthCmd=wget -qO /dev/null http://localhost:8080/api/stats
HealthInterval=30s
[Service]
Restart=on-failure
[Install]
WantedBy=default.target
```
Aktivieren:
```sh
systemctl --user daemon-reload
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
```
Die App fährt bei SIGTERM sauber herunter (laufende Requests werden zu Ende
bedient, dann wird die DB-Verbindung geschlossen).