deploy.md: Caddy-Config, Port 7777, Upload-Anleitung überarbeitet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+93
-32
@@ -1,5 +1,26 @@
|
|||||||
# Deployment mit Podman
|
# 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
|
## Bauen
|
||||||
|
|
||||||
```sh
|
```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
|
Healthcheck kommt stattdessen aus der Quadlet-Unit (s.u.); alternativ
|
||||||
`podman build --format=docker` oder `podman run --health-cmd=...`.
|
`podman build --format=docker` oder `podman run --health-cmd=...`.
|
||||||
|
|
||||||
## Starten
|
## Starten (Port 7777)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
podman run -d --name kver \
|
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-data:/app/data \
|
||||||
-v kver-media:/app/static/media \
|
-v kver-media:/app/static/media \
|
||||||
kver
|
kver
|
||||||
```
|
```
|
||||||
|
|
||||||
- `kver-data` hält die SQLite-DB (`/app/data/kver.db`), `kver-media` die
|
- `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.
|
- 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:
|
Konfiguration über Umgebungsvariablen:
|
||||||
|
|
||||||
| Variable | Default | Bedeutung |
|
| 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 |
|
| `KVER_DB` | `/app/data/kver.db` | Pfad zur SQLite-Datei |
|
||||||
|
|
||||||
## Bestehende Daten übernehmen
|
## Bestehende Daten übernehmen
|
||||||
|
|
||||||
DB und Medien einmalig in die Volumes kopieren (Container muss dafür kurz
|
DB und Medien einmalig in die Volumes kopieren (Volumes müssen existieren;
|
||||||
gelaufen sein bzw. die Volumes existieren):
|
Container kann laufen oder gestoppt sein):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
podman volume create kver-data
|
podman volume create kver-data
|
||||||
podman volume create kver-media
|
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}}')/"
|
# Rootless: podman unshare sorgt dafür, dass die UID ins User-Namespace-
|
||||||
cp static/media/* "$(podman volume inspect kver-media -f '{{.Mountpoint}}')/"
|
# 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
|
Danach Container (neu) starten. Die App legt beim ersten Start alle fehlenden
|
||||||
UID-Zuordnung ins User-Namespace-Mapping passt.)
|
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 ist die einfachste Wahl: automatisches TLS, setzt `X-Forwarded-Proto`
|
||||||
(Caddy/nginx), der `X-Forwarded-Proto: https` setzt — daran erkennt die App
|
und `X-Forwarded-For` ohne Konfiguration, kein eigenes Body-Size-Problem.
|
||||||
HTTPS und setzt das `Secure`-Flag auf den Session-Cookie (siehe `isHTTPS`).
|
|
||||||
|
|
||||||
nginx-Beispiel:
|
`/etc/caddy/Caddyfile` (oder `~/.config/caddy/Caddyfile` bei rootless):
|
||||||
|
|
||||||
```nginx
|
```caddyfile
|
||||||
location / {
|
kver.example.org {
|
||||||
proxy_pass http://127.0.0.1:8080;
|
# Unveränderliche Uploads cachen — Dateinamen sind einmalig (<unix>-<rand>),
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
# Browser lädt jede Datei genau einmal.
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
header /static/media/* Cache-Control "public, max-age=31536000, immutable"
|
||||||
client_max_body_size 20m; # Uploads bis 17 MB durchlassen
|
|
||||||
|
reverse_proxy 127.0.0.1:7777
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`client_max_body_size` muss über dem App-Limit (17 MB, `maxUploadBytes`)
|
Das ist alles. Caddy kümmert sich um Let's-Encrypt-Zertifikat, HTTPS-Redirect
|
||||||
liegen, sonst kappt nginx vor der App.
|
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
|
Optional Body-Limit vor der App (App begrenzt selbst auf 17 MB):
|
||||||
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`
|
```caddyfile
|
||||||
vorschalten — oder damit leben, dass das Limit global statt pro IP wirkt.
|
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)
|
## Autostart mit systemd (Quadlet)
|
||||||
|
|
||||||
@@ -86,11 +139,11 @@ Description=kver (Kontrollverlust)
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
Image=localhost/kver:latest
|
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-data:/app/data
|
||||||
Volume=kver-media:/app/static/media
|
Volume=kver-media:/app/static/media
|
||||||
# Healthcheck hier statt im Containerfile: Podman baut standardmäßig
|
# Healthcheck hier statt im Containerfile: Podman baut OCI-Images,
|
||||||
# OCI-Images, die kennen kein HEALTHCHECK (daher die Warnung beim Build).
|
# die kennen kein eingebettetes HEALTHCHECK.
|
||||||
HealthCmd=wget -qO /dev/null http://localhost:8080/entry/feed/0
|
HealthCmd=wget -qO /dev/null http://localhost:8080/entry/feed/0
|
||||||
HealthInterval=30s
|
HealthInterval=30s
|
||||||
|
|
||||||
@@ -109,11 +162,19 @@ systemctl --user start kver
|
|||||||
loginctl enable-linger "$USER" # damit der Dienst ohne Login läuft
|
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
|
## Update einspielen
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
git pull origin master
|
||||||
podman build -t kver .
|
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
|
Die App fährt bei SIGTERM sauber herunter (laufende Requests werden zu Ende
|
||||||
|
|||||||
Reference in New Issue
Block a user