advanced.md - Virtastic Notepad

OpenMW manual: Advanced hosting

--- title: "Advanced hosting" description: "Run the openmw-web server without Docker, wire your own reverse proxy, operate a multiplayer server, and size a gateway." book: "Self-hosting" order: 52 --- Everything here is for operators going beyond the Docker quick start in [Self-hosting](/openmw/docs/self-hosting/): running without Docker, wiring your own proxy, tuning a public gateway. Most people never need this page. Stuck, or something not covered? Ask in [Discord](https://discord.gg/PzFfDkbSue).

## Running without Docker

The same server, set up by hand. **1. Build.** ```bash cd server npm ci npm run build # emits dist/server.mjs (single world) and dist/gateway.mjs (gateway) ``` **2. Stage game data.** Copy your own `Data Files` into `<dataDir>/gamedata`. A multiplayer server refuses to boot without it - deliberate, not a bug. The sim-peer binary is auto-probed from `/usr/local/bin/openmw`, `/usr/bin/openmw` or `/opt/openmw/bin/openmw` (override with `[simPeer] binary`); the production image below includes one. **3. Generate the vanilla manifest** (until it exists the locker refuses every upload - the safe default): ```bash node server/tools/gen-vanilla-manifest.mjs "/path/to/Morrowind/Data Files" \ --out <dataDir>/vanilla-manifest.json ``` **4. Write `<dataDir>/config.toml`.** Defaults live in [`server/config.default.toml`](https://github.com/Virtastic/openmw-web/blob/main/server/config.default.toml), documented inline; overrides deep-merge over them. Minimum viable: ```toml [server] password = "<long random string>" # the SIM PEER's credential - never typed by a player. [auth] requireSso = true # SSO-only sign-in. Leave unset to also allow passwords. returnUrl = "https://example.com/launcher.html" [auth.google] # and/or [auth.discord] / [auth.microsoft] enabled = true clientId = "..." clientSecret = "..." redirectUri = "https://example.com/auth/google/callback" ``` Two silent footguns, both logged at boot: `requireSso` unset leaves password login open beside SSO; behind Cloudflare, set `[limits] trustCloudflareIp = true` or every player shares one rate-limit bucket. Storage is optional - with no S3 configured, lockers and saves land on the server's disk (set `[locker] publicBase` to the origin players reach it on; provider-side steps in [Multiplayer setup](/openmw/docs/multiplayer-setup/)). **5. Run.** ```bash node dist/server.mjs --data ./devdata --port 8080 # one world node dist/gateway.mjs --worlds /data/worlds --shared /data --port 8080 --base-port 9000 docker compose -f server/docker-compose.prod.yml up -d # or the image ``` S3 keys go in the environment / an `env_file`, never in `config.toml`. **6. Reverse-proxy, same origin as the game page.** The page will not hand its session ticket to a different hostname. Forward to the gateway: ``` /w/* /ws # the gameplay WebSocket - needs Upgrade handling /auth/* # OAuth sign-in /locker/* # game-data upload/stream /saves /saves/* /worlds /worlds/* ``` [`deploy/Caddyfile`](https://github.com/Virtastic/openmw-web/blob/main/deploy/Caddyfile) is a working reference. Non-negotiables it encodes: **strip `CF-Connecting-IP`, `X-Omw-Client-IP` and `True-Client-IP` from client requests** (a forged one grants a fresh login budget and walks past IP bans), preserve `X-Forwarded-Proto`, keep the COOP/COEP/CORP isolation headers on the game page, and never expose `/admin`, `/metrics`, `/healthz` or `/status` to the internet. **7. Verify.** ```bash curl -s localhost:8080/healthz curl -s localhost:8080/auth/providers curl -s localhost:8080/worlds ```

## Configuration knobs you will actually touch

| Key | What | |---|---| | `[server] name`, `motd`, `maxPlayers` | identity and capacity | | `[server] password` | **the sim peer's credential**, not a player password | | `[auth] requireSso`, `[auth.google/discord/microsoft]` | sign-in | | `[locker] *` | storage: S3 endpoint/bucket, or `publicBase` for disk mode; save quota | | `[login] allowRegistration`, `inviteCode`, `resumeWindowSec` | who may join; rejoin window | | `[content] enforce`, `[engine] enforce` | load-order / engine matching | | `[sharing] *`, `[rules] *` | what is shared, and gameplay policy | | `[admin] owners`, `allowConsole` | moderation | | `[cellReset]`, `[limits]`, `[simPeer]`, `[dev] bots` | wipes, rate limits, the peer, test bots |

## Operating a multiplayer server

Ranks per account: **0** player, **1** moderator (kick, tp, tpto on the dashboard console), **2** admin (ban, unban, give, motd), **3** owner (setrank, console). List your account in `[admin] owners` and restart - promoted on boot, no hand-editing. Every action is logged as `admin.action`. `/console` executes a script on a player's own client: owner-only, fully logged, and `[admin] allowConsole = false` removes it entirely. **Endpoints.** `GET /healthz` liveness; `GET /status` the launcher-facing summary (no IPs, no account data); `GET /metrics` Prometheus, gated on `[metrics] token`, 404 while disabled. `SIGUSR1` flushes state; `SIGTERM`/`SIGINT` disconnect cleanly and flush. **Development bots.** `[dev] bots = N` (or `OMW_DEV_BOTS`, capped 16) spawns bots that hold real accounts and handles for testing menus and social flows alone. The server says loudly at boot when they run. Never on a server strangers can reach. **Privacy.** Everything the server stores about players, and how to erase it, is in [`server/PRIVACY.md`](https://github.com/Virtastic/openmw-web/blob/main/server/PRIVACY.md) - including `--delete-account <name>`. Read it before taking sign-ins from anyone but yourself.

## The sim-peer image

The production image (`server/Dockerfile.simpeer`, target `tier2`) compiles OpenMW from source: ```bash docker build --build-arg BUILD_JOBS=6 -f server/Dockerfile.simpeer -t openmw-simpeer . ``` **`BUILD_JOBS` ≈ one per gigabyte of spare RAM.** OpenMW translation units reach 1–2 GB each; ninja's default on a many-core box exhausts memory and presents as a *hang*, not an OOM kill.

## Sizing a gateway

**Every occupied world costs a sim peer** - worlds multiply the cost. Measured (Linux x86-64, full retail data, one player): peer 487 MB + world 136 MB ≈ **640 MB per occupied world**. Re-measure on your hardware with [`server/scripts/measure-capacity.ts`](https://github.com/Virtastic/openmw-web/blob/main/server/scripts/measure-capacity.ts). The ceiling lives on the gateway: ```toml [worlds] memBudgetMb = 8192 # total RAM for worlds and their peers worldCostMb = 640 # measured cost of one occupied world gatewayReserveMb = 256 # held back for the gateway itself ``` `GET /healthz` reports the live ceiling; a refused player is told the server is full. **`memBudgetMb = 0` disables the memory governor entirely** - the combination that gets a container OOM-killed while every per-world cap reads as satisfied. Keep `memBudgetMb + gatewayReserveMb` at or below the container's own limit. **Rolling restart: `kill -HUP` the gateway.** Worlds restart one at a time, emptiest first, each drained and verified back before the next - a world-code deploy is not an outage. `--idle-reap-ms` (default 120000) controls how long a world may sit empty before it is stopped; its data survives and it revives when its owner returns.