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

## 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 (<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
# 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).
