# 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 podman build -t kver . ``` Multi-Stage-Build (`Containerfile`): statisches Go-Binary (CGO aus, modernc- SQLite ist pures Go), 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 \ -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. - 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_DB` | `/app/data/kver.db` | Pfad zur SQLite-Datei | | `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. ## Bestehende Daten übernehmen 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 # 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. Die App legt beim ersten Start alle fehlenden Tabellen und Indizes automatisch per `CREATE ... IF NOT EXISTS` an. ## 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 (-), # 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/entry/feed/0 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, WAL-Checkpoint beim DB-Close).