Hosting docs
Ship your game backend to Boxity Legion: push a Docker image, get a URL, scale to zero when idle.
The whole idea
You push a Docker image of your game backend. Legion runs it on a managed Kubernetes fleet behind a built-in matchmaker at play.stage.bloxity.io: it boots a pod on the first player, fills it to your room cap, spawns another pod when it's full (the Roblox “fill a server, then start a new one” model), and scales back to zero when everyone leaves. Each game also gets an isolated managed Mongo database automatically, and deploys drain (in-progress games finish; nobody is dropped mid-match). Your game keeps its own rooms and rules — Legion handles hosting, URL, TLS, the matchmaker, autoscaling, Mongo and zero-downtime deploys. Steps below; scaling, rooms and deploys are explained at the end.
1. Your backend image
Your backend must be a Docker image that:
- listens on the port given by the PORT env var,
- serves GET /health returning HTTP 200 quickly (the readiness/liveness probes hit it — don't block the event loop for seconds),
- runs as a non-root user,
- (recommended) handles SIGTERM by draining instead of exiting immediately, so a deploy doesn't drop players mid-game (see deploys, below).
Legion injects these env vars at deploy time — read them, don't hardcode:
PORT # the port to listen on (2567)
NODE_ENV=production
CLIENT_ORIGIN # your game site origin (CORS), e.g. https://yourgame.io
JWT_SECRET # strong per-game secret (generated once, stable across deploys)
MONGODB_URI # an ISOLATED managed Mongo db scoped to THIS game+channel — automatic,
# no setup; the scoped user can't read any other game's data
BLOXITY_GAME_ID # your gameId
BLOXITY_CHANNEL # 'prod' or 'dev'
POD_NAME # this pod's name (handy for logs / room reporting)HTTP and WebSocket share the one port (Colyseus works as-is). Managed Mongo is automatic — each game+channel gets its own database and a user scoped to only that database, so you just read MONGODB_URI and connect; no provisioning, no credentials to manage, and no game can reach another's data.
2. Create a game
On My Games, create a game with a lowercase id (e.g. my-game). You get two backend URLs and a deploy token (re-viewable any time on My Games behind the eye icon):
prod https://<gameId>.host.stage.bloxity.io dev https://<gameId>.dev.host.stage.bloxity.io
These URLs are for HTTP only: your client's auth and API calls, and Boxity's webhooks to your server. Your client's multiplayer socket never connects to them; it goes through the matchmaker (step 5).
3. Deploy from GitHub Actions
You set one secret: LEGION_DEPLOY_TOKEN (your token from step 2). Nothing else — the image is pushed to your repo's own GitHub Container Registry with the built-in GITHUB_TOKEN, no DigitalOcean or registry credentials.
# .github/workflows/deploy.yml (dev branch -> dev channel, main -> prod)
permissions: { packages: write }
# 1. docker login ghcr.io (username github.actor, password GITHUB_TOKEN)
# 2. build ./Server and push ghcr.io/<owner>/<game>-server:<channel>-<sha>
# 3. tell Legion to roll it (this is the only Boxity-specific call):
curl -sf -X POST "https://legion.stage.bloxity.io/v1/apps/<gameId>/deploy" \
-H "Authorization: Bearer $LEGION_DEPLOY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"channel":"dev","image":"ghcr.io/<owner>/<game>-server:dev-<sha>","version":"<sha>",
"seatCap":15}'seatCap is your room's player cap (= your Colyseus room's maxClients): when a pod fills to it, the matchmaker spawns another. Set it to what one pod can really simulate. It is optional (default 50) and preserved across deploys. You no longer pass a port — every backend listens on the one port Legion injects. The deploy also provisions your managed Mongo (above).
You don't need to set a pod ceiling. Boxity starts as many pods as your players need, up to a platform limit that lets every game hold at least the platform's minimum player count. maxReplicas can only raise that limit, never lower it.
One-time: after the first push, make the GHCR package public so we can pull it (repo → Packages → Package settings → Change visibility). Your deploy token only works for your own game.
4. Host your frontend too (optional)
You can also host your static client (Unity WebGL, three.js, any SPA) on Boxity. Zip your build and either drag it onto the channel on My Games, or upload by API. A version is required so the dashboard can show whether your frontend matches the deployed backend.
# upload a build zip (same token as deploys). version is required. curl -sf -X POST \ "https://api.stage.bloxity.io/v1/hosting/games/<gameId>/frontend?channel=dev&version=<sha>" \ -H "Authorization: Bearer $LEGION_DEPLOY_TOKEN" \ -H "Content-Type: application/zip" \ --data-binary @build.zip
Served at (point your own domain here with a CNAME, or use it directly):
prod https://<gameId>.play.stage.bloxity.io dev https://<gameId>.dev.play.stage.bloxity.io
Your client's HTTP calls use the matching backend host from step 2. Its multiplayer connection goes through the matchmaker (step 5).
5. Connecting, scaling & rooms
Required: connect through the matchmaker
Never open your game socket to <gameId>.host.stage.bloxity.io, and never call Colyseus joinOrCreate on it. That address is one fixed route to one pod, so the matchmaker never sees your players and never starts a second pod. It works while the game is small. Once one pod holds more players than it can simulate, joins get slow, then fail, and the pod is restarted, which disconnects everyone in it.
Players enter through the matchmaker at play.stage.bloxity.io. The Boxity SDK asks it for a seat, it picks a pod with room (or starts a new one), and you point your Colyseus client at the relay endpoint it returns. HTTP and auth still go straight to <gameId>.host.stage.bloxity.io.
<script src="https://sdk.stage.bloxity.io/v1/legion-sdk.min.js"></script> // load it, don't bundle it
// on every connect: ask the matchmaker, then join through the endpoint it returns
const { endpoint } = await Legion.SDK.net.resolveEndpoint("<gameId>");
const client = new Client(endpoint); // a NEW Client for every resolve
const room = await client.joinOrCreate("yourRoom", { /* join opts */ });- Use the SDK rather than your own POST /v1/play: it reuses the player's seat on retries and carries the party ticket that keeps parties together.
- Each endpoint is pinned to one pod. Build a new Client from each resolve; never reuse one made from an earlier endpoint.
- If the resolve or the join fails, resolve again with a backoff (the matchmaker holds a cold start up to ~45 s; give up after ~60 s). If the room closes unexpectedly, resolve again and rejoin: that is how players move off a pod that is being replaced.
- A page on <gameId>.dev.play.stage.bloxity.io reaches your dev backend automatically. A frontend on your own domain reaches prod; pass { version: "preview" } to reach dev.
- Once you run more than one pod, Gems webhooks to <gameId>.host.stage.bloxity.io can land on any pod. Record the grant in your database (keyed by transactionId) and answer 200; don't look the buyer up in one pod's memory.
Fill, then spawn, then scale to zero. The matchmaker fills a pod to your seatCap, spawns the next pod when it's full (up to maxReplicas), and scales the whole game back to zero after a few idle minutes — the Roblox model. The first connection cold-starts a pod (a second or two). An idle game runs no pods and costs nothing; you pay only for the players you actually hold. Static frontends cost effectively nothing.
Cross-pod room directory (optional). If your server reports its live rooms to the matchmaker (a tiny ~2-second snapshot loop), players can browse and join a specific room by id or code across every pod, not just the one they'd be placed on — a real server browser / join-friend feature. Without it, fill-then-spawn still works; you just don't get the cross-pod browser.
Zero-downtime deploys (drain). On a deploy or scale-down Legion sends SIGTERM and waits up to 10 minutes. If your server traps it and drains — let in-progress matches finish, hop idle/lobby players to the fresh pod — nobody is dropped mid-game, and new players are routed to the new pod automatically. Colyseus' default (disconnect everyone at once) still works, just less gracefully; trapping SIGTERM is the difference between “match finishes” and “everyone reconnects.”
How many players per pod. Each pod gets 1 vCPU / 512 MB. Set seatCap to what one pod's simulation comfortably holds (your room's maxClients); the matchmaker spawns more pods past that. Set it too high and every pod lags; set it honestly and Boxity adds pods as you grow.
Regions. Everything runs in one region today, so there is a single room space. Real multi-region (separate clusters under one global matchmaker) is on the roadmap.