Skip to content

Self-hosting a Java room

A self-hosted node is a headless room host. It connects to a relay server, opens a lobby room, and runs the games for everyone who joins — players only need the web or desktop client. With the java-forge backend the node spawns one Java Forge process per game, so games are played on the original Forge rules engine rather than the Rust port.

Both options below start from a full checkout:

Terminal window
git clone --recurse-submodules https://github.com/witchesofthehill/manabrew.git
cd manabrew

The node authenticates to the relay with the relay’s server key. Set it via SELF_HOSTED_NODE_SERVER_KEY — never commit it anywhere.

The image bundles everything: the node binary, the Java Forge harness, and a JRE. Create a compose.yml:

services:
self-hosted-node:
build:
context: .
dockerfile: forge-engine/crates/self-hosted-node/Dockerfile
environment:
SELF_HOSTED_NODE_RELAY_URL: "wss://relay.manabrew.app"
SELF_HOSTED_NODE_SERVER_KEY: "${SELF_HOSTED_NODE_SERVER_KEY:?required}"
SELF_HOSTED_NODE_ROOM_NAME: "my-room"
RUST_LOG: "self_hosted_node=info"
restart: unless-stopped

The image defaults to the Java backend, so no extra configuration is needed:

Terminal window
SELF_HOSTED_NODE_SERVER_KEY= docker compose up --build

You need a Rust toolchain, a JDK (17+), and Maven.

Build the Java Forge harness jar and the cardset archive once:

Terminal window
mvn -pl forge-harness -am package -DskipTests
cargo run --release -p forge-cardset-archive --features build --bin build-cardset-archive

Then run the node:

Terminal window
SELF_HOSTED_NODE_ROOM_NAME=my-room \
SELF_HOSTED_NODE_SERVER_KEY=<your-server-key> \
SELF_HOSTED_NODE_ENGINE_BACKEND=java-forge \
SELF_HOSTED_NODE_RELAY_URL=wss://relay.manabrew.app \
JAVA_HOME="$(/usr/libexec/java_home)" \
cargo run --release -p self-hosted-node --features java-forge

$(/usr/libexec/java_home) resolves the JDK path on macOS; on Linux point JAVA_HOME at your JDK installation (for example /usr/lib/jvm/temurin-21-jdk).

All settings are environment variables:

VariableDefaultPurpose
SELF_HOSTED_NODE_RELAY_URLws://127.0.0.1:9443Relay server to connect to
SELF_HOSTED_NODE_SERVER_KEYRelay server key (must match the relay’s)
SELF_HOSTED_NODE_ROOM_NAMESelf-Hosted NodeLobby name shown to players
SELF_HOSTED_NODE_ROOM_PASSWORDnoneRequire a password to join
SELF_HOSTED_NODE_FORMATanyGame format (e.g. commander)
SELF_HOSTED_NODE_MAX_PLAYERS4Seats in the room
SELF_HOSTED_NODE_MAX_GAMES1Concurrent games the node will run
SELF_HOSTED_NODE_ENGINE_BACKENDrustjava-forge for the Java engine (java also works)
SELF_HOSTED_NODE_BOT_ENABLEDfalseSeat an AI bot in the room
SELF_HOSTED_NODE_AUTO_STARTfalseStart as soon as the room fills

You don’t have to use the public relay — the relay server (forge-server) is part of the repo and self-hostable too. It handles lobbies, matchmaking, and message relay between players; it never runs games itself.

Terminal window
FORGE_SERVER_KEY=<pick-a-key> cargo run --release -p forge-server

It listens for WebSocket connections on port 9443 (override with FORGE_PORT) and serves a health endpoint on 9444. Point your node and your clients at it with ws://your-host:9443 — or put it behind a TLS-terminating proxy and use wss://. The key you pick is the same one your nodes pass as SELF_HOSTED_NODE_SERVER_KEY.

A Dockerfile is available at forge-engine/crates/forge-server/Dockerfile, and compose.production.yml in the repo root shows a complete relay + node deployment behind Caddy.

The browser client is a static site (yarn build:webdist/), but it is not “just static files”: the game worker uses SharedArrayBuffer, which requires cross-origin isolation. Whatever serves it — and every proxy in front — must deliver these headers on the HTML, worker JS, and WASM responses:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless

If a proxy strips them, the page loads but games won’t start. Verify in DevTools: window.crossOriginIsolated must be true. Also note the web client is not offline-capable — card images come from Scryfall at runtime. ops/Caddyfile in the repo is a working reference configuration.