Hosting Colyseus and Node.js game servers
Gameye runs browser multiplayer servers written in Node.js, including Colyseus rooms and plain ws servers. Each match is one Node.js process in its own Gameye session, on a dedicated server in the region you choose. No Gameye SDK is needed.
How it works
Section titled “How it works”- Write an authoritative server with Colyseus, or with Node.js and
ws, that serves HTTP and the WebSocket upgrade on one TCP port. - Package it as a Linux container and push it to Docker Hub or GitHub Container Registry.
- Create a Gameye application for the image, with that port registered as TCP.
- When your matchmaker forms a match, your backend calls
POST /sessionand sends the players the session’s address. - The process exits when the match ends. That ends the session.
Browser pages served over https need a secure WebSocket, so plan for TLS early.
Prerequisites
Section titled “Prerequisites”- A Gameye account. Sign up at trial.gameye.com.
- An API token for your backend, created in the Admin Panel under Users → API tokens.
- Node.js 22 or newer, and Docker.
Colyseus on Gameye
Section titled “Colyseus on Gameye”Colyseus gives you rooms, matchmaking hooks and schema-based state sync. On Gameye, run one Colyseus process per match:
- Your own matchmaker or backend decides who plays together and starts a Gameye session for the match. Colyseus’s built-in matchmaker then only has to place those players in the single room on that process.
- Listen on the port registered in your Gameye application. Colyseus defaults to
2567, so either register2567or pass your own port. - Exit the process when the room is disposed, so the session ends and you stop paying for it.
import { Room } from "colyseus";
export class MatchRoom extends Room { async onDispose() { await reportResults(); // tell your backend how the match ended setImmediate(() => process.exit(0)); // ends the Gameye session }}Colyseus registers its own shutdown routine by default (gracefullyShutdown), so a stop request from Gameye disposes the room before the process exits.
On the client, connect to the session’s address and join the room:
import { Client } from "colyseus.js";
const client = new Client("wss://s-1234.play.yourgame.example");const room = await client.joinOrCreate("match", { token: joinToken });The port, container and TLS rules below apply to Colyseus unchanged.
A plain Node.js server
Section titled “A plain Node.js server”Each Gameye session runs one container, and each container runs one match. The server is the authority. Clients send inputs, and the server simulates on a fixed tick and broadcasts the state. Bind 0.0.0.0, authenticate each join with a token your backend minted, and exit with code 0 when the match ends or when Gameye sends SIGTERM.
// server.js: one match per processimport { WebSocketServer } from "ws";import { createServer } from "http";
const PORT = Number(process.env.GAME_PORT ?? 8080);const TICK_HZ = 30;
const httpd = createServer((req, res) => { res.writeHead(200); res.end("ok"); });const wss = new WebSocketServer({ server: httpd });
wss.on("connection", (ws) => { const playerId = authenticate(ws); // check a join token your backend minted if (!playerId) return ws.close(4001); ws.on("message", (m) => onInput(playerId, m)); // inputs only, never outcomes ws.on("close", () => onLeave(playerId));});
setInterval(() => { // the server simulates and broadcasts simulate(1 / TICK_HZ); broadcast(wss, snapshot()); if (matchOver()) endMatch();}, 1000 / TICK_HZ);
async function endMatch() { await reportResults(); process.exit(0); // ends the Gameye session}
process.on("SIGTERM", endMatch); // a stop request: drain and exithttpd.listen(PORT, "0.0.0.0");TLS for browser clients
Section titled “TLS for browser clients”A page served over https can’t open ws:// to a raw IP and port, and a Gameye session gives you exactly that. Pick one of these:
- Per-session DNS and a wildcard certificate. When a session starts, your backend creates a record such as
s-<session>.play.yourgame.examplepointing at the session’s IP. The server terminates TLS with a wildcard certificate it fetches from your backend at boot. Never bake the key into the image or pass it in session env. - A WebSocket relay at the edge. The page connects over
wss://to a small relay, such as a Cloudflare Worker, that holds TLS and forwards to the session. Your backend signs a short-lived token naming the session’s address, and the relay connects only to that address.
The container
Section titled “The container”Start node directly as the entrypoint. Under npm start, npm swallows SIGTERM and your drain code never runs. Build for linux/amd64.
FROM node:22-slim AS depsWORKDIR /appCOPY package*.json ./RUN npm ci --omit=dev
FROM node:22-slimWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY server/ ./server/USER nodeEXPOSE 8080/tcpENTRYPOINT ["node", "server/server.js"]Start a session
Section titled “Start a session”When your matchmaker forms a match, your backend starts a session:
curl -X POST https://api.production-gameye.gameye.net/session \ -H "Authorization: Bearer $GAMEYE_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "image": "your-image", "location": "eu-west-1", "env": { "MATCH_ID": "abc-123" } }'The response holds the session’s host and the host port mapped to your container port. Send those to the players, or use them to create the DNS record or relay token from the TLS section. See Custom matchmaker for region selection and player tracking.
See it running: Scrapyard
Section titled “See it running: Scrapyard”Scrapyard is a browser car-combat game whose authoritative server is Node.js and WebSocket. Nakama’s matchmaker forms each quick-play match, and the Gameye Fleet Manager for Nakama starts a Gameye session for it. Browsers reach the session through a Cloudflare Worker relay. GAMEYE.md in the repository walks through it, and scripts/gameye-up.sh runs it on your own Gameye account.
With a coding agent
Section titled “With a coding agent”The gameye-skills web server skill and the gameye-mcp server can scaffold the Dockerfile, check it, and start sessions for you. See AI agents.