How it works
QuakeKube runs ioquake3 dedicated servers on Kubernetes, and ioquake3 built with Emscripten as the browser client. It’s made of a few parts:
- The gateway serves the web app and the browser client, and carries browsers’ game traffic to the servers, since browsers can’t send UDP.
- A QuakeServer is a game server’s spec, stored in the Kubernetes API. The
web app creates them, and so can
kubectl. - The controller turns each QuakeServer into a running game server pod, and runs the master server native clients find servers through.
- Content, a mod and its maps, comes from content images or downloads, and browsers download the same files from the server.
Controller
The controller reconciles QuakeServer objects. For each one it creates:
- a Secret with the server’s configuration and a generated rcon password
- a Deployment running the game server
- a Service for the gateway to reach the server inside the cluster
- a Service exposing the game’s UDP port to native clients, depending on
gameServers.exposure(LoadBalancer, NodePort, a host port or nothing)
The server’s status says when it’s ready, which is once its pod runs the current spec, and has its external address once it’s reachable. When the pod is stuck rather than starting, e.g. because its image can’t be pulled or its content fails to download, the status has a message saying why, which the web app shows.
The controller also runs a Quake 3 master server. Game servers send it heartbeats, and it answers server browsers’ queries with every ready server’s external address.
Game servers
Each game server’s pod runs the q3 command from the game server image:
q3 fetch, an init container, puts the server’s content in place: it downloads pk3s and zip archives, and links the files from content images, which are mounted as image volumes.q3 runadds the Quake 3 demo’s files, turns the configuration into aserver.cfgand runsioq3ded. When the pod stops, it warns players and counts down before shutting the server down.
q3 run also serves, on port 8080, the server’s content and info for browser
clients, its maps’ levelshots, its Prometheus metrics, and a gRPC health check
for the pod’s probes.
When the configuration changes, q3 run changes the running server from its
console where it can: a different map, game type, limits or bots reload the map,
the way a map rotation does, and players stay connected. Only a different mod
restarts the server, and that replaces its pod anyway.
Gateway
The gateway serves the web app, which lists the servers, creates them from the
presets in gateway.presets, and runs the browser client.
The Quake 3 protocol is UDP, which browsers can’t send. The browser client’s traffic goes to the gateway instead, which relays it to the server’s UDP port. It uses WebTransport when it can, whose unreliable datagrams behave like UDP, and falls back to a WebSocket, where one lost packet holds up everything behind it. Since the servers only ever see UDP, they’re plain ioquake3 servers that native clients can join too.
Changing a server
A server’s owner can change it from the web app’s server list, or from the game’s pause menu while playing: its map, game type, bots and limits, or which game it runs. The change is saved in the QuakeServer, but that takes a minute to reach the server: the controller renders the Secret, and Kubernetes updates the file in the pod. So the gateway also sends the server the same configuration right away:
The gateway renders the configuration with the controller’s code and authorizes
it with the server’s rcon password, which it reads from the server’s Secret.
When the Secret’s copy arrives, q3 run finds nothing new and ignores it.
Switching to another game changes the server’s content, so its pod is replaced. Players’ pages notice when the server comes back running the other game and rejoin it. The browser client also rejoins when the server changes to a map whose files it didn’t download, unless it reads files as it needs them.
Idle servers
Servers created from the web app are deleted after nobody has played on them for
gateway.idleTimeout, 30 minutes by default. That includes servers that never
start. Kubernetes deletes their pods and Services with them.
Levelshots
Servers play a rotation of maps, moving to the next one after each match. The web
app shows a picture of each map, its levelshot, which comes from the map’s pak,
with its title and the game types it’s made for, from its arena file. For maps of games no server is running, the gateway runs a pod for
each preset that assembles the preset’s content like a game server’s pod, with
q3 levelshots serving thumbnails of the maps’ levelshots and what their arena
files say about them. The gateway keeps
them in a ConfigMap, quake-kube-levelshots-<preset>, and only makes them again
when the preset’s content or maps change.
Browser client
The play page downloads the server’s content through the gateway, the same files the server has, so it plays the server’s mod and maps. It keeps them in the browser’s cache, keyed by checksum, so they’re only downloaded once and shared between servers. Browsers with JSPI read the paks as the game needs them instead, so joining doesn’t wait for a whole mod to download. Mods have to ship QVMs to run in the browser, since it can’t load native game libraries.
Quake 3 demo EULA
The game servers use the files from the Quake 3 demo, whose End-User License
Agreement must be agreed to before they’re distributed. q3 run won’t start
without --agree-eula, which the controller passes, so installing QuakeKube
means agreeing to it. Browser clients show it to players before downloading the
files.