Skip to content

Hosting the web client

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.

The published image defaults to the official public relay and is repointed at runtime — one image, any relay, no rebuild. Set these on the container:

Env varDefaultPurpose
RELAY_HOST(official relay)Relay host the client connects to
RELAY_PORT443Relay port — 443 dials wss://, else ws://
RELAY_PASSWORDforgeShared relay key (MANABREW_SERVER_KEY)

The entrypoint writes these into /srv/manabrew/config.js on start, which the app reads before connecting. Leaving RELAY_HOST unset keeps the official relay. Players can also add extra servers at runtime from Settings → Server.

If you build the bundle from source instead, bake the same values in at build time with VITE_RELAY_HOST / VITE_RELAY_PORT / VITE_RELAY_PASSWORD; runtime RELAY_* overrides a baked default when both are present.

The published image bundles Caddy with a default :80 config that already sends the isolation headers, so no checkout and no mounted config are needed. Create a compose.yml:

services:
web:
image: ghcr.io/witchesofthehill/manabrew-web:latest
environment:
RELAY_HOST: relay.example.com
RELAY_PORT: "443"
RELAY_PASSWORD: "${MANABREW_SERVER_KEY:-forge}"
ports:
- "80:80"
restart: unless-stopped

Then pull and run:

Terminal window
docker compose up -d

This serves plain HTTP on port 80 — put it behind your own TLS proxy, or mount a Caddyfile with a real hostname over /etc/caddy/Caddyfile for automatic HTTPS:

# ./Caddyfile — replace play.example.com with your domain.
play.example.com {
root * /srv/manabrew
encode zstd gzip
@config path /config.js
handle @config {
header Cache-Control "no-store"
file_server
}
handle {
header {
Cross-Origin-Opener-Policy "same-origin"
Cross-Origin-Embedder-Policy "credentialless"
Cross-Origin-Resource-Policy "same-origin"
}
try_files {path} /index.html
file_server
}
}
# add to the web service:
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config

To serve the client and a relay together behind one Caddy, see the full-stack example; ops/Caddyfile is the full production reference.