Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.1 KiB
Deployment mit Podman
Was muss auf den Server?
Nur der Quellcode — das Image wird direkt auf dem Server gebaut.
# Einmalig: Repo klonen
git clone https://codeberg.org/<user>/kver.git
cd kver
Oder bei bestehendem Checkout:
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
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)
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-datahält die SQLite-DB (/app/data/kver.db),kver-mediadie 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):
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):
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):
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)
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):
[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:
systemctl --user daemon-reload
systemctl --user start kver
loginctl enable-linger "$USER" # damit der Dienst ohne Login läuft
Status prüfen:
systemctl --user status kver
podman healthcheck run kver
Update einspielen
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).