Die Organisation wird angelegt. NewOrganizationRoute.tsx ruft nur mutate(data) ohne onSuccess, ohne Navigation, ohne Meldung — die Seite bleibt einfach stehen. Im Container-Log steht dazu nichts, weil der Handler nur im Fehlerfall protokolliert; genau dieses Schweigen ist der Hinweis, dass es geklappt hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
221 lines
8.4 KiB
Markdown
221 lines
8.4 KiB
Markdown
# d4rk_media
|
||
|
||
Der Medien-Stack: **Fivemanage Lite** mit MySQL und MinIO. Ein Ablageort für
|
||
alle Resourcen — Fahrzeugbilder aus `d4rk_photostudio`, Item-Icons, Marken,
|
||
was sonst noch anfällt.
|
||
|
||
Bisher liegt das auf dem Nextcloud und wird per rclone von Hand hochgeladen.
|
||
Das trägt, solange ein Mensch hochlädt. Sobald ein Script es tun soll, fehlt
|
||
ein Schreibweg über HTTP — und genau den bringt Lite mit, samt Token je
|
||
Resource und einer Oberfläche zum Nachsehen.
|
||
|
||
---
|
||
|
||
## Ausprobieren, bevor es auf den Server geht
|
||
|
||
`0.1.0-beta.23` ist früh. Erst auf Docker Desktop:
|
||
|
||
```bash
|
||
cp .env.example .env # und ausfüllen, siehe unten
|
||
docker compose -f docker-compose.local.yml up -d
|
||
```
|
||
|
||
- Oberfläche: <http://localhost:8080> — `admin` und dein `ADMIN_PASSWORD`
|
||
- MinIO: <http://localhost:9001> — `lite` und dein `MINIO_ROOT_PASSWORD`
|
||
|
||
Wenn das trägt, dasselbe auf dem Server.
|
||
|
||
---
|
||
|
||
## Auf dem Server, über Portainer
|
||
|
||
Portainer kann einen Stack direkt aus diesem Repo ziehen:
|
||
|
||
**Stacks → Add stack → Repository**
|
||
|
||
| Feld | Wert |
|
||
|---|---|
|
||
| Repository URL | `https://git.d4rkst3r.de/D4rkst3r/d4rk_media` |
|
||
| Compose path | `docker-compose.yml` |
|
||
| Environment variables | die vier Geheimnisse plus `PUBLIC_URL` |
|
||
|
||
Die Werte gehören **in die Stack-Variablen**, nicht in eine Datei im Repo.
|
||
`.env` steht deshalb in `.gitignore`.
|
||
|
||
### Wie der Reverse Proxy die Dienste erreicht
|
||
|
||
Über den **Host**, und das steht fertig in `docker-compose.yml`. Genauso laufen
|
||
`d4rkbot` auf 3080 und `cdn-files` auf 8090 schon.
|
||
|
||
```
|
||
fivecdn.d4rkst3r.de -> http://host.docker.internal:9100
|
||
fivemanage.d4rkst3r.de -> http://host.docker.internal:9101
|
||
```
|
||
|
||
**Nicht 9000 und 8080** — die sind auf diesem Server vergeben (Portainer und
|
||
nextcloud-aio). Im Container bleibt es bei 9000 und 8080; nur die Seite zum
|
||
Host wandert. Kollidiert später noch etwas, sind es zwei Variablen:
|
||
`MINIO_HOST_PORT` und `LITE_HOST_PORT`.
|
||
|
||
Die Ports sind an **alle** Adressen gebunden, nicht an `127.0.0.1`. Das ist
|
||
kein Versehen: NPM liegt im Standard-Bridge-Netz (`172.17.0.3`) und kommt über
|
||
die Docker-Brücke herein, nicht über Loopback — ein an `127.0.0.1` gebundener
|
||
Port nimmt diese Anfrage nicht an. **Zu macht sie die Firewall**, nicht die
|
||
Bindung. 9100 und 9101 gehören von außen dicht; öffentlich soll nur NPM auf
|
||
80/443 sein.
|
||
|
||
#### Wenn der Proxy im selben Docker-Netz hängt
|
||
|
||
Dann geht es sauberer, ganz ohne offene Ports — dafür liegt
|
||
`docker-compose.proxynet.yml` bereit. **Portainer nimmt unter *Compose path*
|
||
aber nur eine Datei**; eine Ergänzung wird dort stillschweigend ignoriert. Wer
|
||
diesen Weg will, führt die beiden Dateien zusammen oder startet den Stack von
|
||
Hand:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml -f docker-compose.proxynet.yml up -d
|
||
```
|
||
|
||
Auf **diesem** Server trifft das nicht zu.
|
||
|
||
Die Ports sind an **alle Adressen** gebunden, weil `host.docker.internal` auf
|
||
diesem Server über die Docker-Brücke kommt und ein `127.0.0.1`-Port die
|
||
Anfrage nicht annähme — dieselbe Bindung, mit der `d4rkbot` und `cdn-files`
|
||
schon laufen.
|
||
|
||
**Damit macht die Firewall den Port zu, nicht die Bindung.** 9100 und 9101
|
||
gehören von außen dicht; öffentlich erreichbar soll nur NPM auf 80 und 443
|
||
sein. Wer weiß, dass Loopback bei ihm erreicht wird, setzt
|
||
`BIND_ADDR=127.0.0.1`.
|
||
|
||
---
|
||
|
||
## Die vier Geheimnisse
|
||
|
||
Sie werden **erzeugt**, nicht irgendwo abgeholt. Das Token für die Resourcen
|
||
kommt später und woanders her: aus der laufenden Lite-Oberfläche.
|
||
|
||
**PowerShell** — schreibt die fertige `.env`:
|
||
|
||
```powershell
|
||
function New-Secret([int]$n){ $b = New-Object byte[] $n; [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); ($b | ForEach-Object { $_.ToString('x2') }) -join '' }
|
||
@("DB_PASSWORD=$(New-Secret 16)","MINIO_ROOT_PASSWORD=$(New-Secret 16)","API_TOKEN_HMAC_SECRET=$(New-Secret 32)","ADMIN_PASSWORD=$(New-Secret 12)","PUBLIC_URL=https://fivecdn.d4rkst3r.de","PROXY_NETWORK=web") | Set-Content -Encoding ascii .env
|
||
```
|
||
|
||
Zwei Fallen stecken darin, beide schon hineingetreten:
|
||
|
||
- `RandomNumberGenerator::Create()` und **nicht** `::Fill` — letzteres gibt es
|
||
erst ab .NET 6, Windows PowerShell 5.1 kennt es nicht.
|
||
- `-Encoding ascii` und **nicht** `utf8` — PowerShell 5.1 schreibt sonst ein
|
||
BOM voran, und dann heißt die erste Variable `DB_PASSWORD`.
|
||
- `Get-Random` wäre die naheliegende Wahl und die falsche: es ist nicht
|
||
kryptografisch sicher, und hier wird ein Token-Signaturgeheimnis erzeugt.
|
||
|
||
**Bash / Linux:**
|
||
|
||
```bash
|
||
printf 'DB_PASSWORD=%s
|
||
MINIO_ROOT_PASSWORD=%s
|
||
API_TOKEN_HMAC_SECRET=%s
|
||
ADMIN_PASSWORD=%s
|
||
PUBLIC_URL=https://fivecdn.d4rkst3r.de
|
||
PROXY_NETWORK=proxy
|
||
' "$(openssl rand -hex 16)" "$(openssl rand -hex 16)" "$(openssl rand -hex 32)" "$(openssl rand -hex 12)" > .env
|
||
```
|
||
|
||
---
|
||
|
||
## PostgreSQL, nicht MySQL
|
||
|
||
`0.1.0-beta.23` geht direkt in den PostgreSQL-Treiber; `DB_DRIVER` wird nicht
|
||
ausgewertet. Eine MySQL-DSN endet deshalb in
|
||
|
||
```
|
||
panic: pgdriver: invalid scheme: lite
|
||
```
|
||
|
||
— der Treiber liest sie als URI und findet als Schema den Benutzernamen.
|
||
|
||
Die `docker-compose.test.yml` des Projekts zeigt MySQL, benutzt dafür aber
|
||
`fivemanage/lite:latest` von Docker Hub, und die Reihe steht dort bei
|
||
`beta.16` still. Das README des Projekts hat recht: `postgres://…`.
|
||
|
||
Das Volume heißt deshalb `pgdata` und nicht `db` — wer den Stack schon mit
|
||
MySQL laufen hatte, bekommt so eine frische Ablage, statt dass PostgreSQL über
|
||
ein MySQL-Verzeichnis stolpert. Das alte Volume bleibt als Waise liegen und
|
||
kann weg:
|
||
|
||
```bash
|
||
docker volume rm fivemanager_db
|
||
```
|
||
|
||
---
|
||
|
||
## Zwei Adressen, nicht eine
|
||
|
||
Lite spricht MinIO unter zwei verschiedenen Namen an, und beide muessen
|
||
stimmen:
|
||
|
||
| Variable | Wert | Wofuer |
|
||
|---|---|---|
|
||
| `AWS_ENDPOINT` | `http://minio:9000` | Wie Lite die Dateien **hinlegt** — Containername im internen Netz |
|
||
| `BUCKET_DOMAIN` | `$PUBLIC_URL/media` | Woraus die **oeffentliche** Adresse gebaut wird, die Lite nach dem Upload zurueckgibt |
|
||
|
||
`BUCKET_DOMAIN` steht nicht im README des Projekts, nur in dessen
|
||
`.env.template`. Ohne sie kaemen Adressen zurueck, die nur innerhalb von
|
||
Docker gelten — der Upload saehe erfolgreich aus, und das Bild waere von
|
||
aussen nicht abrufbar.
|
||
|
||
Den Bucket legt Lite beim Start selbst an und setzt dabei die oeffentliche
|
||
Leserichtlinie (`pkg/storage/s3/s3.go`). Es ueberspringt beides, wenn der
|
||
Bucket schon existiert — deshalb legt dieser Stack ihn **nicht** vorher an.
|
||
|
||
---
|
||
|
||
## Zwei Eigenheiten, die Zeit kosten
|
||
|
||
**`traces export: … dial tcp [::1]:4318: connect: connection refused`**, alle
|
||
fünf Minuten. Kein Fehler: Lite schickt Tracing-Daten an einen Collector, den
|
||
dieser Stack bewusst nicht mitbringt. Abschalten geht in `beta.23` **nicht** —
|
||
`otlpEndpoint` ist eine Konstante in `pkg/otel/exporter.go`, `ENV` wählt nur
|
||
zwischen TLS und unverschlüsselt. Weil die Adresse `localhost` ist, würde auch
|
||
ein Jaeger-Container daneben nichts nützen; er müsste sich den Netzwerk-
|
||
Namensraum teilen. Ignorieren.
|
||
|
||
**„Create organization" tut nichts.** Doch — die Organisation wird angelegt,
|
||
nur geht die Oberfläche danach nicht weiter: `NewOrganizationRoute.tsx` ruft
|
||
`mutate(data)` ohne `onSuccess`, ohne Navigation, ohne Meldung. **Seite neu
|
||
laden**, dann findet die App sie. Wer mehrfach geklickt hat, hat mehrere.
|
||
|
||
**Ein Knopf in der Oberfläche tut gar nichts.** Dann steht der Grund im
|
||
Container-Log, nicht auf dem Bildschirm. `web/src/utils/http-util.ts` wirft
|
||
jeden Fehler als `new Error(…)` weiter, die aufrufenden Hooks prüfen aber auf
|
||
`err instanceof ApiError` — das ist danach nie wahr, und der Fehler wird
|
||
verschluckt. 401, 500 und ein falscher Rumpf sehen deshalb alle gleich aus:
|
||
nichts passiert.
|
||
|
||
---
|
||
|
||
## Was hier bewusst fehlt
|
||
|
||
**ClickHouse und Jaeger.** Beide stehen in der Entwicklungs-Compose des
|
||
Projekts und sind für strukturiertes Logging und Tracing da. Zum Ablegen von
|
||
Bildern braucht es sie nicht, und ClickHouse ist eine schwere Abhängigkeit.
|
||
Nachrüsten geht jederzeit — die App findet sie über `CLICKHOUSE_*`.
|
||
|
||
**Offene Ports in der Betriebsfassung.** Nur der Reverse Proxy spricht mit der
|
||
App; Datenbank und MinIO bleiben im internen Netz. Die Entwicklungs-Compose des
|
||
Projekts macht 5432, 9000, 16686 und mehr auf — dort in Ordnung, hier nicht.
|
||
|
||
---
|
||
|
||
## Herkunft
|
||
|
||
Zusammengestellt aus [fivemanage/lite](https://github.com/fivemanage/lite):
|
||
dessen `README.md`, `deployments/docker-compose.yml` (Entwicklung, ohne die
|
||
App) und `deployments/docker-compose.test.yml` (zeigt die App-Verdrahtung und
|
||
dass MySQL genügt).
|
||
|
||
**Nicht laufen gelassen.** Rechne mit ein, zwei Anläufen — vor allem beim
|
||
ersten Login und beim Bucket.
|