Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
!ui
!ui/**
!server.py
!metrics.py
!Dockerfile
**/__pycache__/
**/*.py[cod]
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Syntax-check UI server
run: python3 -m py_compile server.py scripts/patch-idle-cpu.py
- name: Syntax-check Python entrypoints
run: python3 -m py_compile server.py metrics.py scripts/patch-idle-cpu.py

python-tests:
runs-on: ubuntu-latest
Expand All @@ -46,6 +46,8 @@ jobs:
run: python3 tests/test_auto_backup.py
- name: Server-control (systemctl dispatch) tests
run: python3 tests/test_server_control.py
- name: Metrics exporter tests
run: python3 tests/test_metrics.py
- name: ServerDescription schema tests
run: python3 tests/test_schema.py
- name: HTTP integration tests
Expand Down Expand Up @@ -102,7 +104,7 @@ jobs:
- uses: actions/checkout@v4
- name: Validate example JSON
run: |
python3 -c "import json,sys; [json.load(open(f)) for f in ['scripts/ServerDescription_example.json','scripts/WorldDescription_example.json']]"
python3 -c "import json,sys; [json.load(open(f)) for f in ['scripts/ServerDescription_example.json','scripts/WorldDescription_example.json','helm/windrose/dashboards/windrose-overview.json']]"

kustomize-render:
runs-on: ubuntu-latest
Expand All @@ -125,3 +127,10 @@ jobs:
helm template windrose ./helm/windrose >/dev/null
helm template windrose ./helm/windrose --set serverConfig.mode=managed >/dev/null
helm template windrose ./helm/windrose --set serverConfig.mode=mutable >/dev/null
helm template windrose ./helm/windrose \
--set metrics.enabled=true \
--set metrics.serviceMonitor.enabled=true \
--set metrics.grafanaDashboard.enabled=true >/dev/null
! helm template windrose ./helm/windrose \
--set metrics.enabled=true \
--set metrics.port=28080 >/dev/null
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ COPY --chmod=755 scripts/reconcile-engine-ini.sh /usr/local/bin/reconcile-engine
# the same image as the game binary; the UI sidecar runs via a command
# override at /opt/windrose-ui/server.py. Backend lives at the repo
# root (server.py); frontend bundle is the sibling ui/ tree.
COPY --chown=10000:10000 server.py /opt/windrose-ui/server.py
COPY --chown=10000:10000 ui/ /opt/windrose-ui/ui/
RUN chmod 755 /opt/windrose-ui/server.py
COPY --chown=10000:10000 server.py /opt/windrose-ui/server.py
COPY --chown=10000:10000 metrics.py /opt/windrose-ui/metrics.py
COPY --chown=10000:10000 ui/ /opt/windrose-ui/ui/
RUN chmod 755 /opt/windrose-ui/server.py /opt/windrose-ui/metrics.py

USER steam

Expand Down
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ This is a community project. It is not affiliated with or endorsed by the Windro

The Windrose dedicated-server Steam app (id `4129620`) pulls fine via anonymous SteamCMD — that's the default bootstrap, so a fresh pod / droplet / compose stack goes from nothing to a running server without any WindowsServer tarball work. Save data lives on persistent storage and survives game patches automatically. The admin console also exposes an upload path for operators running a pre-release or modded `WindowsServer/` build; see *Optional: Bring Your Own Server Files* below.

The pod runs three containers:
The pod runs three containers by default:

- **`windrose`** — the game itself under GE-Proton. Only runs the game binary; no backgrounded work in its shell (Proton hates shell job-control races with Xvfb).
- **`xvfb`** — a dedicated X display server on `:99`, shared into the game container via an `emptyDir` at `/tmp/.X11-unix`. Lives in its own container so its signal space can't interfere with Proton.
- **`windrose-ui`** — a stdlib-only Python admin console (served from the same image as the game container via `python3 /opt/windrose-ui/server.py`). Exposes the invite-code card, server/players/resources status, config editor, backups, per-world editor, manual `WindowsServer` upload, and Discord/generic webhook dispatch. Shares the PVC with the game container so both see the same filesystem.

All three share the pod's PID namespace (`shareProcessNamespace: true`) so the UI sidecar can `pgrep` for the game process.
All three share the pod's PID namespace (`shareProcessNamespace: true`) so the UI sidecar can `pgrep` for the game process. Helm can also add an opt-in **`windrose-metrics`** sidecar for Prometheus scraping.

Other Windrose dockerizations exist — this one leans on patterns we already operate in [`enshrouded-self-hosted`](https://github.com/shipstuff/enshrouded-self-hosted): GE-Proton, non-root container, PVC-backed persistence, host networking, Helm + plain manifests + Docker Compose in sync.

Expand Down Expand Up @@ -140,15 +140,24 @@ for the full env list; it's commented inline.

All three containers come up: `windrose` (game, `network_mode: host`), `xvfb` (display server), `windrose-ui` (UI on `127.0.0.1:28080` by default).

Enable the optional Prometheus exporter sidecar with the `metrics` compose profile:

```bash
docker compose --profile metrics up -d
curl -s http://127.0.0.1:9464/metrics
```

## Install On Bare Linux

```bash
sudo ./bare-linux/install.sh
```

Three systemd system services (game + Xvfb + admin UI), running as a
non-root `steam` user. UI binds to `127.0.0.1` by default; override
with `UI_BIND=0.0.0.0 UI_PASSWORD=…` at install time. See
Three systemd system services (game + Xvfb + admin UI), plus an optional
Prometheus metrics service, running as a non-root `steam` user. UI binds
to `127.0.0.1` by default; override with
`UI_BIND=0.0.0.0 UI_PASSWORD=…` at install time. Enable the metrics
exporter with `WINDROSE_METRICS_ENABLED=true`. See
[`bare-linux/README.md`](bare-linux/README.md) for sizing, swap recipe,
and the pre-loaded-world workflow (recommended for small VPSes).

Expand Down Expand Up @@ -205,6 +214,13 @@ Every variable below is consumed by the container entrypoint, so it applies iden
| `UI_PASSWORD` | `` | HTTP basic-auth password; empty = no auth (only safe on LAN-only / firewalled hosts). Username is ignored. |
| `UI_ENABLE_ADMIN_WITHOUT_PASSWORD` | `false` | Explicit opt-in for destructive endpoints when `UI_PASSWORD` is empty. With a password set, destructive is always allowed. |
| `UI_SERVE_STATIC` | `true` | Set `false` to have the Python sidecar serve only `/api/*`; pair with an nginx that owns the static bundle. |
| `UI_ENABLE_METRICS_ROUTE` | `false` | Optional simple-install mode: expose Prometheus metrics at the admin UI's `/metrics` route. Kubernetes should prefer the dedicated metrics sidecar. |

**Metrics exporter**
| Env var | Default | Purpose |
|---|---|---|
| `METRICS_BIND` | `0.0.0.0` | Bind address for standalone `python3 /opt/windrose-ui/metrics.py`. |
| `METRICS_PORT` | `9464` | Prometheus scrape port for the standalone exporter. |

**Backups**
| Env var | Default | Purpose |
Expand All @@ -229,6 +245,34 @@ Every variable below is consumed by the container entrypoint, so it applies iden
| `WINDROSE_WEBHOOK_POLL_SECONDS` | `15` | Poll cadence for the event detector thread. |
| `WINDROSE_WEBHOOK_TIMEOUT` | `5` | HTTP POST timeout (seconds). |

## Prometheus Metrics And Grafana

The metrics exporter is stdlib Python in [`metrics.py`](metrics.py). It can run as a separate process (`python3 /opt/windrose-ui/metrics.py`) or be imported by the admin server for an opt-in `/metrics` route. Kubernetes installs should use the dedicated sidecar so metrics stay isolated from the admin UI.

![Grafana dashboard: Windrose server metrics](docs/screenshots/03-grafana-dashboard.jpg)

Helm example:

```yaml
metrics:
enabled: true
serviceAnnotations:
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "9464"
prometheus.io/job: windrose-canary
serviceMonitor:
enabled: false
grafanaDashboard:
enabled: true
```

Use either `metrics.serviceMonitor.*` for Prometheus Operator or `metrics.serviceAnnotations` for plain Prometheus annotation discovery. If your Prometheus install only selects `ServiceMonitor`s with a release label, set it under `metrics.serviceMonitor.labels`. If Grafana only watches a monitoring namespace for dashboard ConfigMaps, set `metrics.grafanaDashboard.namespace`.

**Multiple servers.** Grafana does not discover Windrose servers directly; it asks Prometheus for `job` and `instance` label values on `windrose_exporter_scrape_success`. If Prometheus only scrapes canary, canary is the only option in the dashboard. Prometheus attaches target labels such as `job` and `instance` to every scrape, so the packaged dashboard can view all Windrose servers together or drill into one. For annotation-based Prometheus installs, set a unique `prometheus.io/job` per server/release so the dropdown is readable; for Compose or bare-Linux, use distinct Prometheus scrape jobs or relabel `instance` to a friendly server name.

The exporter intentionally publishes aggregate operational state only: running status, player counts, process CPU/RSS/uptime, resource ceilings, staged config/world/mod changes, mod counts, backup counts, backend region, save version, and build identity from Steam/logs. It does not publish invite codes, player account IDs, or player names.

## Update The Server On Game Patch

When Windrose ships a patch, the dedicated-server binary bumps its `<GameVersion>` save-path segment. Entrypoint migrates worlds forward automatically when it sees ≥2 version folders under `RocksDB/`.
Expand Down Expand Up @@ -398,6 +442,7 @@ All routes are served by the `windrose-ui` container at `:28080`. Static assets
| Method | Path | Auth | Purpose |
| ------ | --------------------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
| GET | `/healthz` | open | Liveness — returns `ok`. Safe for k8s probes and external monitors. |
| GET | `/metrics` | open | Optional Prometheus scrape endpoint when `UI_ENABLE_METRICS_ROUTE=true`. Prefer the metrics sidecar on k8s. |
| GET | `/`, `/app.css`, `/app.js`, `/index.html` | open | Served when `ui.serveStatic=true` (default). Disable if nginx serves the static assets. |
| GET | `/api/status` | open / authed | Game process state, player list, resource usage, invite code, backend region, staged-change hints. Public view redacts `AccountId`s and omits `allowDestructive` / `stagedWorlds`. |
| GET | `/api/invite` | authed | Plain-text invite code. |
Expand Down Expand Up @@ -511,10 +556,11 @@ kubectl kustomize . >/dev/null
helm lint ./helm/windrose
helm template windrose ./helm/windrose >/dev/null
shellcheck scripts/entrypoint.sh scripts/pack-windowsserver.sh
python3 -m py_compile server.py
python3 -m py_compile server.py metrics.py
python3 tests/test_retention.py
python3 tests/test_restore.py
python3 tests/test_auto_backup.py
python3 tests/test_metrics.py
python3 tests/test_http.py
bash tests/test_api.sh # requires a running canary — see CLAUDE.md
```
Expand Down
46 changes: 41 additions & 5 deletions bare-linux/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Bare Linux Install

Run Windrose directly on a spare Linux box (Ubuntu 22.04+ / Debian 12+) as
three systemd system services: game + Xvfb + admin UI. Validated on an
Ubuntu 24.04 DigitalOcean droplet with 2 cores / 4 GiB RAM; should run
three systemd system services by default: game + Xvfb + admin UI. A fourth
Prometheus metrics exporter service is available as an opt-in. Validated on
an Ubuntu 24.04 DigitalOcean droplet with 2 cores / 4 GiB RAM; should run
anywhere the [`Dockerfile`](../Dockerfile) deps are available.

## Sizing
Expand Down Expand Up @@ -67,6 +68,9 @@ See § What The Install Includes for the full picture.
- `windrose-xvfb.service` — virtual display on `:99`
- `windrose-game.service` — the game under GE-Proton
- `windrose-ui.service` — the Python admin console (stdlib, no deps)
- **Optional metrics unit**:
- `windrose-metrics.service` — Prometheus exporter on `127.0.0.1:9464`
when `WINDROSE_METRICS_ENABLED=true`
- **Files under `/opt/windrose/`** (the installed code) and
**`/home/steam/windrose/`** (the PVC-equivalent: game binaries,
saves, backups). `/etc/windrose/windrose.env` holds all runtime knobs,
Expand Down Expand Up @@ -98,6 +102,33 @@ sudo journalctl -fu windrose-game
sudo journalctl -fu windrose-ui
```

## Prometheus Metrics

Enable the standalone metrics exporter at install time:

```bash
sudo WINDROSE_METRICS_ENABLED=true ./bare-linux/install.sh
curl -s http://127.0.0.1:9464/metrics
```

The installer writes `windrose-metrics.service` on every run, but only
enables and starts it when `WINDROSE_METRICS_ENABLED=true`. Re-run with
`WINDROSE_METRICS_ENABLED=false` to disable and stop the service.

Defaults are loopback-only:

```env
WINDROSE_METRICS_ENABLED=false
METRICS_BIND=127.0.0.1
METRICS_PORT=9464
```

For a Prometheus running on the same host, scrape `127.0.0.1:9464`. For a
remote Prometheus, either use an SSH tunnel/reverse proxy or set
`METRICS_BIND=0.0.0.0` and firewall the port. The metrics endpoint has no
auth; it does not expose invite codes or player identities, but it does
expose operational state.

## Overrides

All env vars are read from `/etc/windrose/windrose.env`. The installer
Expand All @@ -119,6 +150,10 @@ Or just edit the env file and `systemctl restart windrose-game` after.
| `UI_PORT` | `28080` | UI listen port |
| `UI_PASSWORD` | empty | HTTP basic-auth password. Strongly recommended for any publicly-reachable host. |
| `UI_ENABLE_ADMIN_WITHOUT_PASSWORD` | `false` | explicit opt-in for destructive routes when no password is set — LAN-only |
| `UI_ENABLE_METRICS_ROUTE` | `false` | expose `/metrics` from `windrose-ui` instead of, or in addition to, the standalone metrics service |
| `WINDROSE_METRICS_ENABLED` | `false` | enable/start `windrose-metrics.service` |
| `METRICS_BIND` | `127.0.0.1` | metrics exporter listen iface |
| `METRICS_PORT` | `9464` | metrics exporter listen port |
| `SERVER_NAME` | `Windrose Bare-Linux` | informational |
| `MAX_PLAYER_COUNT` | `4` | 4 is the vendor guide; up to 10 with more RAM |
| `WORLD_NAME` | `Default Windrose World` | display name |
Expand Down Expand Up @@ -246,11 +281,12 @@ the game's RSS spikes on world load + backend handshake.
```
/opt/windrose/scripts/entrypoint.sh # game launcher
/opt/windrose/server.py # admin console
/opt/windrose/metrics.py # Prometheus exporter
/opt/windrose/ui/{index.html,app.js,app.css}
/etc/windrose/windrose.env # runtime env (root-rw, group-r for steam)
/home/steam/windrose/ # game data (WindowsServer/, saves, backups)
/home/steam/steamcmd/ # SteamCMD + GE-Proton compat data
/etc/systemd/system/windrose-{xvfb,game,ui}.service
/etc/systemd/system/windrose-{xvfb,game,ui,metrics}.service
```

The admin console writes backups into `/home/steam/backups/<utc>/`
Expand All @@ -259,8 +295,8 @@ The admin console writes backups into `/home/steam/backups/<utc>/`
## Uninstall

```bash
sudo systemctl disable --now windrose-game windrose-ui windrose-xvfb
sudo rm /etc/systemd/system/windrose-{game,ui,xvfb}.service
sudo systemctl disable --now windrose-game windrose-ui windrose-xvfb windrose-metrics
sudo rm /etc/systemd/system/windrose-{game,ui,xvfb,metrics}.service
sudo systemctl daemon-reload
# Data under /home/steam/ stays — delete manually if desired:
# sudo userdel -r steam
Expand Down
Loading
Loading