This commit is contained in:
Marek Lenczewski
2026-04-17 17:33:23 +02:00
commit 51b6309584

339
SETUP.md Normal file
View File

@@ -0,0 +1,339 @@
# Windrose Dedicated Server auf Hetzner CPX22 — Runbook
Anleitung für einen Agent zum Aufsetzen eines privaten Windrose-Dedicated-Servers
(max. 4 Spieler, Invite-Code-only) auf einem frischen Hetzner CPX22 mit Ubuntu 24.04.
## Eckdaten
- **Hardware:** CPX22 = 2 vCPU AMD EPYC, **4 GB RAM**, 80 GB NVMe SSD.
- **RAM-Risiko:** Windrose verlangt offiziell min. 8 GB für 2 Spieler. Wir kompensieren mit
8 GB Swap und cappen `MaxPlayerCount=4`. Wenn Performance schlecht ist → Hetzner-Console
→ Rescale auf CPX31 (8 GB RAM, ~€13/Monat), kein Datenverlust.
- **Server ist Windows-only.** Lauf über Wine + Xvfb (headless).
- **Setup-Variante:** Bare-Metal mit systemd (geringerer RAM-Footprint als Docker).
- **Connectivity:** Spieler joinen per Invite-Code. UDP 7777/7778 müssen aber trotzdem
reachable sein (NAT-Punch-Through über die öffentliche IP).
- **SteamCMD App-ID:** `4129620` (Windrose Dedicated Server, anonymous).
## Regeln für den ausführenden Agent
- Schritte **2, 3, 6 als root**, Schritte **4, 5, 7 als User `windrose`** (`sudo -iu windrose`).
- **Idempotent** arbeiten: vor jedem destruktiven Schritt prüfen, ob der Zielzustand schon
existiert (Swapfile, WineHQ-Repo, systemd-Unit, …) und ggf. überspringen.
- **Nicht-interaktiv**: bei `apt install` immer `-y`. Für `steamcmd` vorher die Lizenz
per debconf akzeptieren (siehe Schritt 2.6).
- Nach Schritt 5.2 den **InviteCode** aus `ServerDescription.json` extrahieren und im
finalen Report zurückmelden — den braucht der User zum Joinen.
- **Verifikation (Schritt 8) zwingend** ausführen und Ergebnisse berichten.
- **Nicht** ohne Rückfrage den Server neu starten, Snapshots löschen oder die
Hetzner-Cloud-Firewall ändern. Bei Fehlschlag: Log + Hypothese melden, nicht stumm
reparieren.
- Erwartete Dauer: 1525 Min. (SteamCMD-Download ~3 GB + winetricks vcrun2022).
---
## Schritt 1 — Hetzner-Console (manuell durch den User)
Vor dem Server-Setup in der Hetzner Cloud Console:
1. **Snapshot** des frischen CPX22 anlegen (Rollback-Punkt).
2. **Cloud-Firewall** anhängen mit Inbound-Regeln:
- SSH: TCP 22 (am besten nur eigene IP)
- Windrose Game: **UDP 7777** (any source)
- Windrose Query: **UDP 7778** (any source)
3. Outbound: alles erlauben (default). Wichtig für STUN/TURN nach
`*.windrose.support:3478` (UDP/TCP).
---
## Schritt 2 — System-Vorbereitung (als root)
```bash
# 2.1 Basis-Update
apt update && apt -y full-upgrade
# 2.2 Server-User
id windrose >/dev/null 2>&1 || adduser --disabled-password --gecos "" windrose
usermod -aG sudo windrose
# 2.3 Swap (8 GB)
if ! swapon --show | grep -q '/swapfile'; then
fallocate -l 8G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
echo 'vm.swappiness=10' > /etc/sysctl.d/99-swappiness.conf
sysctl --system
# 2.4 Multiarch + Repos
dpkg --add-architecture i386
add-apt-repository -y multiverse
add-apt-repository -y universe
# 2.5 WineHQ-Repo (stable, aktueller als Distro-Wine)
mkdir -pm755 /etc/apt/keyrings
wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
wget -NP /etc/apt/sources.list.d/ \
https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources
apt update
# 2.6 SteamCMD-Lizenz vorab akzeptieren (sonst hängt apt interaktiv)
echo steam steam/question select "I AGREE" | debconf-set-selections
echo steam steam/license note '' | debconf-set-selections
# 2.7 Pakete
DEBIAN_FRONTEND=noninteractive apt install -y --install-recommends winehq-stable
DEBIAN_FRONTEND=noninteractive apt install -y \
software-properties-common lib32gcc-s1 steamcmd \
xvfb cabextract winbind ufw fail2ban unzip jq wget
# 2.8 winetricks
if [ ! -x /usr/local/bin/winetricks ]; then
wget -O /usr/local/bin/winetricks \
https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks
chmod +x /usr/local/bin/winetricks
fi
```
---
## Schritt 3 — Lokale Firewall + SSH-Härtung (als root)
```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 7777/udp
ufw allow 7778/udp
ufw --force enable
systemctl enable --now fail2ban
```
In `/etc/ssh/sshd_config` setzen (nur wenn SSH-Key bereits funktioniert):
```
PasswordAuthentication no
PermitRootLogin prohibit-password
```
Dann `systemctl reload ssh`.
---
## Schritt 4 — Server-Installation (als User `windrose`)
```bash
sudo -iu windrose bash <<'EOF'
mkdir -p ~/steam ~/log ~/backup
cd ~/steam
# 4.1 SteamCMD: Server-Files (forciert Windows-Platform)
steamcmd \
+@sSteamCmdForcePlatformType windows \
+force_install_dir /home/windrose/steam/windrose \
+login anonymous \
+app_update 4129620 validate \
+quit
# 4.2 Wine-Prefix initialisieren
export WINEPREFIX=/home/windrose/steam/windrose/pfx
export WINEARCH=win64
xvfb-run -a wineboot --init
xvfb-run -a winetricks -q win10 vcrun2022
EOF
```
> `vcrun2022` verhindert die häufigsten "missing MSVC runtime"-Crashes.
---
## Schritt 5 — Server-Config
### 5.1 Start-Script `/home/windrose/steam/start_windrose.sh`
```bash
#!/usr/bin/env bash
set -e
export WINEPREFIX=/home/windrose/steam/windrose/pfx
export WINEARCH=win64
export WINEDEBUG=-all
export DISPLAY=:99
if ! pgrep -f "Xvfb :99" >/dev/null; then
Xvfb :99 -screen 0 1024x768x16 &
sleep 2
fi
cd /home/windrose/steam/windrose
exec wine64 WindroseServer.exe
```
`chmod +x /home/windrose/steam/start_windrose.sh`
### 5.2 Erst-Start (Config-Generierung)
Als `windrose`:
```bash
~/steam/start_windrose.sh &
SERVER_PID=$!
sleep 60 # warten bis Erst-Init durch ist
kill $SERVER_PID 2>/dev/null || true
sleep 5
```
Damit werden `ServerDescription.json` und der erste Welt-Ordner unter
`R5/Saved/SaveProfiles/Default/RocksDB/<version>/Worlds/<world_id>/` erzeugt.
### 5.3 ServerDescription patchen
Datei finden + patchen:
```bash
CFG=$(find /home/windrose/steam/windrose -maxdepth 4 -name ServerDescription.json | head -n1)
# InviteCode für Report extrahieren
INVITE=$(jq -r '.ServerDescription_Persistent.InviteCode' "$CFG")
# ServerName + MaxPlayer setzen, kein Passwort
jq '.ServerDescription_Persistent.ServerName = "Marhas Windrose"
| .ServerDescription_Persistent.MaxPlayerCount = 4
| .ServerDescription_Persistent.IsPasswordProtected = false
| .ServerDescription_Persistent.Password = ""' \
"$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
echo "INVITE_CODE=$INVITE" # !!! Dem User zurückmelden !!!
```
---
## Schritt 6 — systemd Service (als root)
`/etc/systemd/system/windrose.service`:
```ini
[Unit]
Description=Windrose Dedicated Server
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=windrose
Group=windrose
WorkingDirectory=/home/windrose/steam
ExecStart=/home/windrose/steam/start_windrose.sh
Restart=always
RestartSec=10
StandardOutput=append:/home/windrose/log/windrose.log
StandardError=append:/home/windrose/log/windrose.log
MemoryHigh=3G
MemoryMax=3500M
[Install]
WantedBy=multi-user.target
```
Aktivieren:
```bash
systemctl daemon-reload
systemctl enable --now windrose
systemctl status windrose --no-pager
```
---
## Schritt 7 — Update- & Backup-Helfer (als User `windrose`)
### 7.1 Update-Script `/home/windrose/steam/update_windrose.sh`
```bash
#!/usr/bin/env bash
set -e
sudo systemctl stop windrose
steamcmd \
+@sSteamCmdForcePlatformType windows \
+force_install_dir /home/windrose/steam/windrose \
+login anonymous \
+app_update 4129620 validate \
+quit
sudo systemctl start windrose
```
### 7.2 Backup-Script `/home/windrose/steam/backup_windrose.sh`
```bash
#!/usr/bin/env bash
set -e
TS=$(date +%Y%m%d-%H%M%S)
SRC=/home/windrose/steam/windrose/R5/Saved
DST=/home/windrose/backup/windrose-${TS}.tar.gz
tar -czf "$DST" -C "$(dirname "$SRC")" "$(basename "$SRC")"
ls -1t /home/windrose/backup/windrose-*.tar.gz | tail -n +8 | xargs -r rm
```
Beide ausführbar machen: `chmod +x ~/steam/{update,backup}_windrose.sh`
### 7.3 Cron (täglich 04:00) als `windrose`
```
0 4 * * * /home/windrose/steam/backup_windrose.sh >> /home/windrose/log/backup.log 2>&1
```
---
## Schritt 8 — Verifikation (zwingend)
```bash
systemctl status windrose --no-pager
ss -ulnp | grep -E '7777|7778' || echo "WARN: Ports nicht gebunden"
free -h
tail -n 50 /home/windrose/log/windrose.log
vmstat 5 3 # Swap-Aktivität: si/so sollten ~0 sein im Idle
```
Erwartet:
- Service `active (running)`
- UDP 7777 + 7778 gebunden
- Idle-RAM <2 GB belegt, Swap fast leer
- Log zeigt "Server ready" o.ä., keine Wine-Stacktraces
Im Spiel-Client: "Join via Invite Code" → den im Report ausgegebenen Code eingeben.
---
## Eskalation bei Problemen
- **OOM / extreme Lags:** Hetzner-Console → Server stoppen → "Rescale" auf CPX31 → starten. ~3 Min., kein Datenverlust.
- **Wine-Crashes beim Start:** `journalctl -u windrose -n 200` nach `wine: Unhandled` durchsuchen, ggf. `winetricks -q dotnet48` nachschieben oder `winehq-staging` statt `winehq-stable` testen.
- **Clients finden Server nicht:** Hetzner-Cloud-Firewall checken (UDP 7777/7778 wirklich offen?), Outbound zu `*.windrose.support:3478` nicht geblockt.
---
## Kritische Dateien
| Pfad | Zweck |
|---|---|
| `/home/windrose/steam/windrose/WindroseServer.exe` | Server-Binary |
| `/home/windrose/steam/windrose/ServerDescription.json` | ServerName, InviteCode, MaxPlayer |
| `…/R5/Saved/SaveProfiles/Default/RocksDB/<ver>/Worlds/<id>/WorldDescription.json` | Welt-Konfig |
| `/home/windrose/steam/windrose/pfx` | Wine-Prefix |
| `/home/windrose/steam/start_windrose.sh` | Launcher (Xvfb + wine) |
| `/etc/systemd/system/windrose.service` | systemd-Unit |
| `/home/windrose/log/windrose.log` | Server-Log |
| `/home/windrose/backup/` | tägliche Save-Backups |
---
## Final-Report-Template (vom Agent zurück an User)
```
Status: success | failed
Invite-Code: <CODE>
Server-Name: Marhas Windrose
Max-Spieler: 4
Public-IPv4: <hetzner-ip>
Service-Status: <output von systemctl status windrose>
RAM-Idle: <free -h>
Swap-Aktivität: <vmstat>
Letzte Log-Zeilen: <tail -n 20 windrose.log>
Auffälligkeiten / Warnings: …
```