Colyseus
Overview
Colyseus is an open-source, authoritative multiplayer game server framework for Node.js. It provides the server half of a realtime multiplayer game: rooms that hold a match or a lobby, schema-based state synchronisation that sends only deltas to every connected client over WebSocket, matchmaking, a server-side simulation loop, and an admin Monitor panel for watching live rooms and clients.
Colyseus is authoritative by design. Clients send messages, and only the server changes state, so a modified client cannot rewrite the game world.
This SecureAnyCloud image runs Colyseus 0.17.10 on Ubuntu 24.04 LTS and Node.js 24 LTS, with a working example room, under a systemd service and an unprivileged colyseus system account. It needs no external database. Colyseus's default local driver keeps the room registry and room state in memory on the single VM, so there is nothing else to provision.
nginx is the only front door. The Node process binds 127.0.0.1:2568 and is never directly reachable. Port 80 carries game traffic and the admin Monitor panel. Port 2567 carries game traffic only. It is the port that Colyseus client SDKs use by default, so clients can use the default endpoint.
What you get:
- Colyseus 0.17.10 with
@colyseus/monitor0.17.8 and the@colyseus/sdk0.17.43 client, at exact pinned versions - Node.js 24 LTS from NodeSource and the TypeScript toolchain, so you can write and build your own rooms on the VM
- A complete example room at
/opt/colyseus/src/rooms/GameRoom.tsshowing schema state, authoritative message handling, a simulation loop and authentication - The admin Monitor panel on
:80/monitor, gated by HTTP Basic Auth with a credential unique to your VM - Game traffic on
:80and on:2567(the client SDK default port), with WebSocket upgrade - A join token unique to your VM: matchmaking refuses any client that does not present it. No default login and no default token ship in the image
colyseus.serviceandnginx.serviceas systemd units, enabled and active- An unauthenticated
/healthzendpoint, proxied to the Node process, for Azure Load Balancer health probes - 24/7 SecureAnyCloud support
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.
Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting size. Choose a larger size as your concurrent player count grows.
Configure the network security group (NSG) to allow inbound 22/tcp from your management network and 80/tcp for the Monitor panel and game traffic. Also allow 2567/tcp if you want clients to use the Colyseus SDK default port. Colyseus serves plain HTTP and WebSocket, so for production terminate TLS in front of it with your own domain. Clients can then use wss://.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Colyseus by SecureAnyCloud, and select Create.
On the Basics tab, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80).
Select Review + create, then Create. If you want the SDK default port, add an inbound rule for TCP 2567 afterwards.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:colyseus-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name colyseus \
--image sac1647283583153:colyseus-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name colyseus --port 80 --priority 1010
az vm open-port --resource-group <your-rg> --name colyseus --port 2567 --priority 1020Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>Step 4 - Confirm the services are running
systemd runs Colyseus as the unprivileged colyseus user. nginx is the only front door. It listens on :80 and :2567 and reverse-proxies to the Node process on 127.0.0.1:2568, including the WebSocket upgrade that carries room state.
Confirm that the services are active and that the application port is bound to loopback only:
systemctl is-active colyseus nginx colyseus-firstboot
sudo ss -tlnp | grep -E ':80 |:2567 |:2568 'Expected output:
active active active LISTEN 0 511 0.0.0.0:2567 0.0.0.0:* users:(("nginx",pid=6351,fd=7),...) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=6351,fd=5),...) LISTEN 0 511 127.0.0.1:2568 0.0.0.0:* users:(("MainThread",pid=6333,fd=22)) LISTEN 0 511 [::]:2567 [::]:* users:(("nginx",pid=6351,fd=8),...) LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=6351,fd=6),...)Step 5 - Retrieve your join token and Monitor credential
On the first boot of every VM, colyseus-firstboot.service generates two secrets unique to that VM: a room join token and a Monitor panel username and password. It writes them, with the URLs you need, to a root-only file. Read the file with:
sudo cat /root/colyseus-credentials.txtThe image contains no default or shared credential. Colyseus has no authentication of its own, and upstream @colyseus/monitor is an unauthenticated admin panel. This image adds both gates and generates their secrets per VM.
The Monitor username is randomised, so it is not admin. The shipped image contains an empty join token and an empty password file, so a captured image cannot be joined or signed in to until first boot has run. Store both secrets somewhere safe.
Step 6 - Confirm the versions and health endpoints
nginx proxies an unauthenticated /healthz to the Node process, so a 200 means the application itself is answering, not merely that nginx is up. Confirm the runtime and framework versions, and check that both public ports respond:
node --version
node -p "require('/opt/colyseus/node_modules/colyseus/package.json').version"
node -p "require('/opt/colyseus/node_modules/@colyseus/sdk/package.json').version" curl -sI http://127.0.0.1/healthz | head -1 curl -s -o /dev/null -w 'Monitor unauthenticated (:80): %{http_code}\n' http://127.0.0.1/monitor/ curl -s -o /dev/null -w 'Game port healthz (:2567): %{http_code}\n' http://127.0.0.1:2567/healthzExpected output:
v24.18.0 0.17.10 0.17.43 HTTP/1.1 200 OK Monitor unauthenticated (:80): 401 Game port healthz (:2567): 200The 401 is expected. It shows that the Monitor panel is closed to anyone without your VM's credential.
Step 7 - Verify Monitor authentication
The whole /monitor path sits behind nginx HTTP Basic Auth, using a bcrypt password file that first boot wrote for this VM alone. Verify all three cases from the VM:
MU=$(sudo grep '^COLYSEUS_MONITOR_USERNAME=' /root/colyseus-credentials.txt | cut -d= -f2-)
MP=$(sudo grep '^COLYSEUS_MONITOR_PASSWORD=' /root/colyseus-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'no credential: %{http_code}\n' http://127.0.0.1/monitor/
curl -s -o /dev/null -w 'wrong password: %{http_code}\n' -u "${MU}:wrong-password" http://127.0.0.1/monitor/
curl -s -o /dev/null -w 'this VM credential: %{http_code}\n' -u "${MU}:${MP}" http://127.0.0.1/monitor/Expected output:
no credential: 401 wrong password: 401 this VM credential: 200The Monitor panel is never served on port 2567. That listener carries game traffic only and returns 404 for /monitor.
Step 8 - Verify the join token gate
Game clients do not use the Monitor credential. They present the join token in their join options, and the room's onAuth check rejects anything else. A stranger who finds your public IP cannot create or join rooms on your server.
Verify both outcomes:
JT=$(sudo grep '^COLYSEUS_JOIN_TOKEN=' /root/colyseus-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'join with no token: %{http_code}\n' \
-X POST -H 'Content-Type: application/json' --data '{}' \
http://127.0.0.1/matchmake/joinOrCreate/game
curl -s -X POST -H 'Content-Type: application/json' --data "{\"token\":\"${JT}\"}" \
http://127.0.0.1/matchmake/joinOrCreate/gameExpected output: the first call is refused and the second returns a seat reservation.
join with no token: 401 {"name":"game","sessionId":"pcJIjJb74","roomId":"96f0xgaJH","processId":"s0bThgqpa"}Step 9 - Verify authoritative state synchronisation
The image ships an end-to-end check of the core job of a multiplayer game server. It connects two real clients with the @colyseus/sdk client library through the nginx front door, then asserts that the server owns the state:
JT=$(sudo grep '^COLYSEUS_JOIN_TOKEN=' /root/colyseus-credentials.txt | cut -d= -f2-)
cd /opt/colyseus && sudo COLYSEUS_JOIN_TOKEN="${JT}" node tools/state-sync-check.mjsExpected output:
no-token join rejected: invalid join token placeholder-token join rejected: invalid join token two clients joined room 96f0xgaJH as xqof_Ta41 + wiz0cEe4P state replicated to peer: players.size = 2 onJoin applied join options: name = alice authoritative move applied server-side and replicated to peer: x=42 y=99 server simulation loop advancing: tick 2 -> 7 STATE_SYNC_OKEach line of the middle section confirms a distinct property:
- Both clients appear in the second client's copy of the state, so state is replicated.
- Client one sends the
movemessage, but the server applies it and client two sees the result. Clients never write to each other. - The simulation tick keeps advancing on its own, which shows the server-side game loop running.
Step 10 - Sign in to the Monitor panel
Browse to http://<vm-public-ip>/monitor/. When your browser prompts for a username and password, enter the Monitor credential from Step 5. nginx refuses unauthenticated requests before they reach Colyseus.
For production, put the panel behind your own hostname with TLS termination (your own reverse proxy or Azure Application Gateway) so the credential is not sent over plain HTTP. Also restrict :80 to your management network in the NSG.
Step 11 - Watch live rooms
After you sign in, the Monitor opens on its room list. It shows every room alive on the server, with its room ID, the name it was defined under (game here), how many clients are connected, its client limit, whether it is locked, and how long it has been running.
Process counters across the top show connections, room count, CPU and memory, so you can see the server's load at a glance. Keep this view open while a game is live.
Step 12 - Inspect a room's clients
Choose INSPECT on a room to open it. The room view shows whether the room is locked, how many client slots are filled and how large its state is. It then lists every connected client by session ID.
From here you can send a message to an individual client, broadcast to the whole room, or dispose of the room.
Step 13 - Read the room's authoritative state
Open the STATE tab. It shows the room's state exactly as the server holds it: the players map keyed by session ID, each entry with the name from its join options and the x/y the server applied from that client's move messages. It also shows the tick counter that the simulation loop advances.
When a client claims something the server disagrees with, this view shows which one is right.
Step 14 - Connect your own game client
Connect your clients with a Colyseus client SDK and pass the join token in the join options. The 0.17 client library is the @colyseus/sdk package. The older colyseus.js package is for servers up to 0.16 and is not wire compatible with this server.
import { Client } from "@colyseus/sdk"; const client = new Client("ws://<vm-public-ip>"); // or ws://<vm-public-ip>:2567 const room = await client.joinOrCreate("game", { token: "<COLYSEUS_JOIN_TOKEN>", name: "alice", }); room.onStateChange((state) => { console.log("players:", state.players.size, "tick:", state.tick); }); room.send("move", { x: 10, y: 20 }); // the SERVER applies thisSDKs are available for Unity, Unreal, Godot, Defold, Haxe, Cocos and the browser. See the Colyseus client documentation. For production, front the server with TLS and use wss://.
Step 15 - Write your own room
The example room is a starting point. You write your own game logic. Rooms live in /opt/colyseus/src/rooms/ and are registered in /opt/colyseus/src/index.ts. The TypeScript toolchain is already installed on the VM.
The development cycle is edit, build, restart:
sudo -e /opt/colyseus/src/rooms/GameRoom.ts
# or add a new room file
sudo -e /opt/colyseus/src/index.ts
# gameServer.define("my_room", MyRoom); cd /opt/colyseus && sudo npm run build
sudo systemctl restart colyseusCarry two things over from the example room:
- Keep the
static onAuth()check. Without it, anyone who finds your server's address can join. - In Colyseus 0.17 the room generic is an options object,
Room<{ state: GameState }>. The pre-0.17 formRoom<GameState>no longer compiles.
See the Colyseus room documentation for details.
Maintenance
- Configuration. The runtime environment lives in
/etc/colyseus/colyseus.env(ownedroot:colyseus, mode 0640). It holds the join token and the loopback bind address. After you change a value, runsudo systemctl restart colyseus. - Your game code.
/opt/colyseus/srcis the source of truth and/opt/colyseus/buildis the compiled output. Keep your rooms in version control outside the VM. - Logs. The server logs to the journal. Run
sudo journalctl -u colyseus -fto follow it. - Security updates. Unattended security upgrades are enabled, so the OS keeps itself patched. Reboot when a new kernel is installed.
- Upgrading Colyseus. The dependency set is pinned exactly in
/opt/colyseus/package.json. Check the upstream migration notes first, because Colyseus makes breaking changes across minor versions. Then bump the versions there, runsudo npm install && sudo npm run build, and restart the service. - TLS. For production, front ports 80 and 2567 with your own domain and a TLS-terminating reverse proxy or Azure Application Gateway, and have clients use
wss://. - Rotate secrets. To force a fresh join token and Monitor credential, remove
/var/lib/sac/data/colyseus-firstboot.doneand/var/lib/colyseus/.bootstrap-ready, then reboot. First boot regenerates both.
Support
SecureAnyCloud provides 24/7/365 expert technical support for this image. Contact sales@secureanycloud.com.
Colyseus is licensed under the MIT License. Colyseus Cloud is a separate commercial hosted service from the Colyseus project and is not part of this image. This image is provided by SecureAnyCloud. Additional charges apply for build, maintenance and 24/7 support.
Deploy on Azure
Launch Colyseus on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com