# Betrieb Für den Betreiber des Portals: Installation auf dem DMZ-Server hinter OPNsense und Traefik, Einspielen der Mod bei Mine-Hoster, laufender Betrieb und Fehlerbehebung. ## 1. Voraussetzungen - Server in der DMZ mit Docker und Docker Compose v2 - Traefik **v3** mit Docker-Provider, einem HTTPS-Entrypoint und Cert-Resolver; Traefik hängt an einem externen Docker-Netz (z. B. `traefik`) - DNS-Eintrag für die Portal-Domain, der auf die öffentliche IP der OPNsense zeigt - Zugang zum Mine-Hoster-Gamepanel (SFTP-Daten unter *Einstellungen → SFTP-Details*) > Traefik **v2**: Die Middleware heißt dort `ipwhitelist` statt `ipallowlist`. In `deploy/docker-compose.yml` > die Zeile `traefik.http.middlewares.atm-ingest-allow.ipallowlist.sourcerange` entsprechend umbenennen. ### Ausgehender Proxy (z. B. Squid in der DMZ) Muss der Server über einen Proxy ins Internet, trage ihn in `deploy/.env` ein: ```bash HTTP_PROXY=http://squid-proxy.example.local:3128 HTTPS_PROXY=http://squid-proxy.example.local:3128 NO_PROXY=localhost,127.0.0.1,.local ``` Compose reicht die Werte als Build-Argumente an beide Images und zur Laufzeit an api und worker weiter. Die Container-Namen `api`, `db` und `web` hängt es automatisch an `NO_PROXY` an, sodass interner Verkehr nie über den Proxy läuft. Eine Proxy-Einstellung für den Docker-Daemon (`/etc/systemd/system/docker.service.d/proxy.conf`) gilt nur für Image-Pulls. Für `npm ci` im Build reicht sie **nicht**; ohne die Einträge oben bricht der Build mit „npm error Exit handler never called!“ ab. Whitelist für den Proxy (alle Verbindungen sind `CONNECT …:443`): | Domain | Wofür | Wann | |---|---|---| | `registry.npmjs.org` | npm-Pakete | Build, Pflicht | | `fonts.googleapis.com`, `fonts.gstatic.com` | Schriften des Dashboards | Build, Pflicht | | `registry-1.docker.io`, `auth.docker.io`, `production.cloudflare.docker.com` | Basis-Images (über die Daemon-Einstellung) | Pull, Pflicht | | `mc-heads.net` | Spielerköpfe | Laufzeit, optional (ohne: graue Platzhalter) | Der SFTP-Abgleich zum Gameserver ist eine direkte SSH-Verbindung und läuft nicht über den Proxy; dafür braucht der DMZ-Server eine eigene Firewall-Freigabe (siehe OPNsense). ## Welche Compose-Datei? | Aufbau | Datei | |---|---| | Traefik läuft auf **demselben** Docker-Host und findet die Dienste über Labels | `deploy/docker-compose.yml` (Abschnitte 2 ff.) | | Traefik läuft auf **einem anderen** Server, oder Docker läuft in einem Proxmox-LXC-Container | `deploy/docker-compose.standalone.yml` (Abschnitt 2a) | ## 2a. Eigenständiger Server, z. B. Proxmox-LXC, Traefik auf einem anderen Server In unprivilegierten LXC-Containern richtet Docker oft kein NAT für seine Bridge-Netze ein: Container erreichen dann weder DNS noch Proxy noch das Internet (Build bricht mit „npm error Exit handler never called!“ ab). Die Standalone-Variante nutzt deshalb für Build und Betrieb das Host-Netzwerk des LXC-Containers. 1. **Ressourcen:** Für den ersten Build 4 GB RAM einplanen (Next.js-Build und drei parallele `npm ci`), im Betrieb reichen 2 GB. 2. **Konfiguration:** ```bash cp deploy/.env.example deploy/.env nano deploy/.env ``` - `TRUST_PROXY` = IP des Traefik-Servers (nur er darf die Client-IP weitergeben) - `INGEST_HMAC_SECRET` und `POSTGRES_PASSWORD` (z. B. mit `openssl rand -hex 32`; Sonderzeichen sind erlaubt) - bei Bedarf `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` (siehe „Ausgehender Proxy“) - `DOMAIN`, `TRAEFIK_*` und `INGEST_ALLOWED_IPS` werden in dieser Variante nicht genutzt (das regelt Traefik selbst, siehe unten) 3. **Starten:** ```bash sh deploy/build.sh # Images im Host-Netzwerk bauen (nach jedem git pull wiederholen) docker compose -f deploy/docker-compose.standalone.yml --env-file deploy/.env up -d docker compose -f deploy/docker-compose.standalone.yml --env-file deploy/.env ps # api, web, db: healthy docker compose -f deploy/docker-compose.standalone.yml --env-file deploy/.env exec api node dist/cli.js admin:create docker compose -f deploy/docker-compose.standalone.yml --env-file deploy/.env exec api node dist/cli.js token:create ``` Die Dienste lauschen auf dem LXC-Container: Web auf Port **3000**, API auf Port **3001**. PostgreSQL lauscht nur auf `127.0.0.1:5432`. Die Ports 3000, 3001 und 5432 müssen frei sein. 4. **Proxmox-Firewall** (die Netzwerkkarte hat `firewall=1`): Ports 3000 und 3001 eingehend **nur vom Traefik-Server** erlauben. Wer die API direkt erreicht, umgeht die IP-Allowlist für `/api/ingest`. Tokens und Signatur schützen den Datenempfang zwar weiterhin, aber die Allowlist ist eine zusätzliche Hürde. 5. **Traefik** (auf dem anderen Server) als dynamische Datei-Konfiguration, z. B. `atm10-stats.yml`: ```yaml http: routers: atm-ingest: rule: "Host(`stats.example.de`) && PathPrefix(`/api/ingest`)" priority: 200 entryPoints: [websecure] middlewares: [atm-ingest-allow] service: atm-api tls: { certResolver: letsencrypt } atm-api: rule: "Host(`stats.example.de`) && PathPrefix(`/api`)" priority: 100 entryPoints: [websecure] service: atm-api tls: { certResolver: letsencrypt } atm-web: rule: "Host(`stats.example.de`)" priority: 10 entryPoints: [websecure] service: atm-web tls: { certResolver: letsencrypt } middlewares: atm-ingest-allow: ipAllowList: # Traefik v2: ipWhiteList sourceRange: ["203.0.113.10/32"] # öffentliche IP des Mine-Hoster-Gameservers services: atm-api: loadBalancer: servers: [{ url: "http://:3001" }] atm-web: loadBalancer: servers: [{ url: "http://:3000" }] ``` Keine Kompressions- oder Buffering-Middleware auf `/api/live` (Live-Updates per Server-Sent Events). **Content-Security-Policy:** Next.js braucht `script-src 'self' 'unsafe-inline'`. Mit `script-src 'self'` allein bleibt die Seite leer (getestet: 38 CSP-Fehler, kein Inhalt). Steht OPNsense vor Traefik, muss Traefik die echte Client-IP sehen, sonst greift die Allowlist nicht (ggf. `forwardedHeaders.trustedIPs` am Entrypoint). 6. Weiter mit Abschnitt 3 (Mod) bzw. 4 (SFTP). Alle späteren Befehle mit `-f deploy/docker-compose.standalone.yml`. ## 2. Portal installieren (Traefik auf demselben Docker-Host) ```bash git clone atm10-stats && cd atm10-stats cp deploy/.env.example deploy/.env openssl rand -hex 32 # Wert für INGEST_HMAC_SECRET openssl rand -hex 24 # Wert für POSTGRES_PASSWORD nano deploy/.env # DOMAIN, TRAEFIK_*, INGEST_ALLOWED_IPS, Secrets, optional SFTP_* docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build docker compose -f deploy/docker-compose.yml --env-file deploy/.env ps # api, web, db: healthy ``` Admin-Konto anlegen (Passwort mindestens 12 Zeichen, wird interaktiv abgefragt) und Ingest-Token für die Mod erzeugen: ```bash docker compose -f deploy/docker-compose.yml --env-file deploy/.env exec api node dist/cli.js admin:create lucas docker compose -f deploy/docker-compose.yml --env-file deploy/.env exec api node dist/cli.js token:create ``` Das Token wird **nur einmal** angezeigt. Später lässt es sich unter `/admin` erneuern. ### Routing | Pfad | Ziel | Hinweis | |---|---|---| | `https://DOMAIN/api/ingest*` | api:3001 | nur von `INGEST_ALLOWED_IPS` (Middleware `atm-ingest-allow`) | | `https://DOMAIN/api/*` | api:3001 | öffentliche Lese-API, `/api/live` (SSE), `/api/admin/*` | | `https://DOMAIN/*` | web:3000 | Dashboard | `/api/live` ist ein Server-Sent-Events-Stream. Auf diese Route keine Kompressions- oder Buffering-Middleware legen. Die API sendet dafür `X-Accel-Buffering: no` und alle 25 s einen Heartbeat. ### OPNsense 1. **Eingehend**: HTTPS (443) zur DMZ/Traefik wie für andere Dienste. Die Einschränkung von `/api/ingest` auf den Gameserver übernimmt Traefik (`INGEST_ALLOWED_IPS`). Die IP des Mine-Hoster-Servers ergibt sich aus der Serveradresse im Gamepanel (Hostname per `nslookup` auflösen). Optional zusätzlich in OPNsense: Alias mit dieser IP und eine eigene Regel nur für den Ingest-Host. 2. **Ausgehend (nur für den SFTP-Abgleich)**: DMZ-Server → SFTP-Host/Port des Mine-Hoster-Servers (TCP). Ohne diese Regel meldet `/sources` „Fehler beim letzten Abgleich“. ## 3. Mod bei Mine-Hoster einspielen Die Mod ist **rein serverseitig**: Spieler installieren nichts, Clients ohne die Mod können normal verbinden. 1. Mod bauen (oder fertiges JAR aus einem Release verwenden): ```bash ./mod/gradlew -p mod build # → mod/build/libs/atmstats-1.0.0.jar ``` 2. Im Gamepanel den Server **stoppen**. 3. Per SFTP (WinSCP/FileZilla) `atmstats-1.0.0.jar` nach `mods/` hochladen. 4. Server starten und nach dem Start wieder stoppen. Dabei entsteht `config/atmstats-server.toml`. 5. Konfiguration per SFTP bearbeiten: ```toml [atmstats] enabled = true endpoint = "https://stats.example.de" serverId = "atm10" token = "" hmacSecret = "" statsSnapshotMinutes = 5 questScanMinutes = 2 healthIntervalSeconds = 60 maxOutboxMb = 50 ``` 6. Server starten und in `logs/latest.log` nach `atmstats:` suchen: - `atmstats: backend reachable`: fertig, Daten laufen. - `atmstats: backend NOT reachable: …`: siehe Abschnitt 6. Die Ereignisse bleiben in `atmstats/outbox/` auf dem Gameserver und werden gesendet, sobald die Verbindung klappt. Ohne `endpoint`, `token` oder `hmacSecret` bleibt die Mod passiv und schreibt nur eine Hinweiszeile ins Log. **ATM10-Updates**: Die Mod verwendet nur APIs, die in NeoForge 21.1.x stabil sind, und deklariert NeoForge ab 21.1.0. Bei Pack-Updates innerhalb von Minecraft 1.21.1 bleibt das JAR gültig. Der Mod-Ordner wird bei Modpack-Updates manchmal ersetzt: danach prüfen, ob `mods/atmstats-*.jar` noch vorhanden ist. ## 4. SFTP-Abgleich (optional, empfohlen) In `deploy/.env` `SFTP_HOST`, `SFTP_PORT`, `SFTP_USER`, `SFTP_PASSWORD` setzen (Gamepanel → Einstellungen → SFTP-Details). `SFTP_ROOT` ist das Serververzeichnis, `SFTP_WORLD_DIR` der Weltordner (Standard `world`, `level-name` aus `server.properties`). Danach `docker compose … up -d worker`. Der Worker holt alle 15 Minuten nur geänderte Dateien. Unter `/admin` lässt sich ein Abgleich sofort anstoßen. Ohne funktionierende Mod liefert SFTP trotzdem Gesamtzähler, Advancements, Quests und (aus dem Log) Beitritte; Echtzeit-Daten, Todesursachen und Serverleistung fehlen dann. Das SFTP-Passwort ist das des Gamepanels und gewährt Schreibzugriff. Der Worker liest ausschließlich. ## 5. Laufender Betrieb ### Updates ```bash git pull docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build ``` Datenbankmigrationen laufen beim Start von api und worker automatisch (serialisiert über einen Advisory-Lock). ### Token und Secret erneuern - **Ingest-Token**: `/admin` → *Ingest-Token erneuern*. Das alte Token wird sofort ungültig. Das neue in `config/atmstats-server.toml` eintragen und den Gameserver neu starten. Bis dahin puffert die Mod und meldet `authentication failed` im Log; es geht nichts verloren, solange das Outbox-Limit nicht erreicht ist. - **HMAC-Secret**: in `deploy/.env` und in der Mod-Konfiguration gleichzeitig ändern, dann api und Gameserver neu starten. ### Backups Der Container `backup` erstellt täglich `pg_dump`-Dateien im Volume `backups` und löscht sie nach `BACKUP_KEEP_DAYS` (Standard 14). Sofortiges Backup: ```bash docker compose -f deploy/docker-compose.yml --env-file deploy/.env exec backup sh /backup.sh once ``` Wiederherstellung (ersetzt alle Daten): ```bash C="docker compose -f deploy/docker-compose.yml --env-file deploy/.env" $C stop api worker web $C exec backup ls -t /backups # gewünschte Datei wählen $C exec backup dropdb atm $C exec backup createdb atm $C exec backup pg_restore --dbname=atm --no-owner /backups/.dump $C start api worker web ``` Das Volume `backups` liegt auf demselben Host. Für echte Ausfallsicherheit die Dateien zusätzlich extern sichern (z. B. per Borg/Restic vom Docker-Volume-Pfad). ### Verhalten bei Neustarts | Situation | Verhalten | |---|---| | Gameserver stoppt normal | Mod schreibt `player_leave` und `server_stop` in die Outbox und sendet sie beim nächsten Start; nach 10 min ohne Health-Meldung schließt der Worker offene Sitzungen | | Gameserver stürzt ab | beim nächsten Start schließt `server_start` offene Sitzungen zum letzten Lebenszeichen (kein Aufblähen der Spielzeit) | | Backend nicht erreichbar | Mod puffert auf der Festplatte (max. `maxOutboxMb`, 7 Tage), sendet mit Backoff 1 s … 5 min weiter; bei Platzmangel werden zuerst alte Statistik-Snapshots verworfen | | Doppelte Lieferung | jede Meldung hat eine eindeutige ID, Duplikate werden ignoriert | | API/Worker-Neustart | zustandslos; der Worker setzt den Log-Abgleich am gespeicherten Byte-Offset fort | ## 6. Fehlerbilder | Meldung | Ursache | Lösung | |---|---|---| | Mod: `backend NOT reachable: …ConnectException` / `HttpTimeoutException` | Mine-Hoster blockiert ausgehende Verbindungen, DNS falsch oder OPNsense/Traefik lässt nicht durch | `curl -v https://DOMAIN/api/health` von außen testen; Traefik-Log prüfen (403 = IP nicht in `INGEST_ALLOWED_IPS`); falls der Hoster ausgehend blockiert: `enabled = false` und nur SFTP nutzen | | Mod: `clock skew: server clock differs …` | Uhr des Gameservers oder des DMZ-Servers weicht > 5 min ab | NTP auf dem DMZ-Server prüfen; beim Hoster melden, falls der Gameserver falsch geht | | Mod: `authentication failed (bad_token)` | Token falsch oder widerrufen | neues Token erzeugen und eintragen | | Mod: `authentication failed (bad_signature)` | `hmacSecret` ≠ `INGEST_HMAC_SECRET` | Werte angleichen | | Mod: `batch rejected by backend … moved to outbox/rejected` | Daten passten nicht zum Protokoll (z. B. veraltete Mod-Version) | Datei in `atmstats/outbox/rejected/` samt `.reason.txt` prüfen, Mod und Backend auf gleichen Stand bringen | | Mod: `outbox limit reached, dropped …` | Backend länger nicht erreichbar | Verbindung reparieren; ggf. `maxOutboxMb` erhöhen | | `/sources`: SFTP „Fehler beim letzten Abgleich“ | Zugangsdaten, Port oder OPNsense-Regel | Fehlertext unter `/admin` → *Letzte Fehler* | | Live-Ticker bleibt leer, Seite sonst ok | Proxy puffert SSE | keine Kompressions-/Buffering-Middleware auf `/api/live` | | Build: `npm error Exit handler never called!` nach ~2 min | `npm ci` erreicht `registry.npmjs.org` nicht (ausgehender Proxy/Firewall, oder Docker im LXC ohne NAT) | Proxy in `deploy/.env` eintragen und Domains freischalten (Abschnitt 1, „Ausgehender Proxy“) | | Build: Fehler beim Laden von Google Fonts | `fonts.googleapis.com`/`fonts.gstatic.com` nicht freigeschaltet | in der Proxy-Whitelist ergänzen | | Nur graue Spielerköpfe | `mc-heads.net` nicht erreichbar | in der Proxy-Whitelist ergänzen (optional) | | Build: `additional privileges requested: pass "--allow=network.host"` | `docker compose` darf nicht im Host-Netzwerk bauen | Images mit `sh deploy/build.sh` bauen, dann `docker compose … up -d` ohne `--build` | | Seite bleibt leer, Browser-Konsole meldet „Content Security Policy“ | CSP am Reverse Proxy erlaubt keine Inline-Scripts | `script-src 'self' 'unsafe-inline'` (Abschnitt 2a) | | Login unter `/admin` klappt nicht ohne HTTPS | Cookie ist `Secure` | Portal über HTTPS aufrufen (lokal: `NODE_ENV=development`) | Spieler, die nicht im Portal erscheinen möchten, lassen sich unter `/admin` ausblenden: Sie verschwinden aus allen öffentlichen Ansichten, Ranglisten und Live-Meldungen. Die Daten bleiben gespeichert, sodass sich das Ausblenden rückgängig machen lässt.