# 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: 15–25 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//Worlds//` 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//Worlds//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: Server-Name: Marhas Windrose Max-Spieler: 4 Public-IPv4: Service-Status: RAM-Idle: Swap-Aktivität: Letzte Log-Zeilen: Auffälligkeiten / Warnings: … ```