What a builder boot sees is now what a node does: - First boots (untouched, or -as-deployed with exactly a new server's settings) are deployed by the node-agent's BootTest in keep mode; the builder only watches the console and inspects what's left. The Go copy of the deploy (its own docker run, mounts and startup prefix) is gone. It had drifted: it gave the image's VOLUME folders their own volumes, which nodes don't, so saves there looked safe while every start loses them. - Files written outside /server and /data are counted whatever their type (it only counted config-like extensions, so .sav or .db saves went unseen), including the image's VOLUME folders; logs, pid and lock files and system folders are left out. - Boot tests ask the server to stay up holdSec (60 s, test.yaml can set it) after its ready line and to listen on every published container port (needs the node-agent's matching BootTest). - Steam apps are downloaded once per build into a gbr.cache volume and copied into each boot, so later boots only check the files (STEAM_CACHE=off). - RUNNER.md no longer suggests docker system prune, which deletes the gbr-boottest network when nothing is using it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.0 KiB
7.0 KiB
Writing a game template
House rules for games/<id>/game.yaml and test.yaml. They apply to people and to the AI builder alike;
the builder reads this file as its instructions. Most rules exist because breaking them broke a real server.
gbrgames validate and gbrgames inspect-image -game <id> enforce what they can.
How a server actually runs
Know this before writing anything:
- The node-agent creates one Docker volume per server and mounts it at both
/serverand/data. Game files, saves and config live there. Anything written elsewhere is lost when the container is recreated, which happens on every start. - The container runs as
1000:1000unless the template setscontainerUser. Never as root. - Every key in
defaultConfig(merged with the owner's changes) reaches the container as an environment variable, named by the agent's rule: insert_before an upper-case run that follows a lower-case letter, then upper-case everything.ServerName→SERVER_NAME,MaxPlayers→MAX_PLAYERS,RCONPort→RCONPORT(no underscore: no lower-case letter beforeP),bIsPvP→B_IS_PV_P. - If
configFilePathis set, the same settings are also written to that file inconfigFileFormatbefore each start. defaultStartupCommand, when set, runs asbash -c "… cd /server && <command>"; otherwise the image's own entrypoint runs.installMethod: STEAMCMDinstallsinstallConfig.appIdinto/serverwith anonymous SteamCMD before the server starts.- Nodes reach a server's RCON, REST and query ports over the Docker network, never through published ports.
Rules
Passwords and secrets
- Admin, RCON and REST passwords:
defaultConfigvalue"", and theconfigSchemaproperty gets"x-generate": {"type": "password"}. The panel then gives every server its own password. Never put a placeholder or default password in a template. Shared defaults are readable by anyone from the public game list. - Join passwords stay
"", with"x-savedValue": {"key": "GAME_JOIN_PASSWORD", "label": "Game join password", "secret": true}. If the server can't start without one, give it"x-generate": {"type": "password"}as well, so every new server gets its own. Boot tests run with exactly what a new server gets: empty settings stay empty. - No real credentials, tokens or IDs anywhere in this repo. A value a server can't start without (a
player ID, a license token) goes in
test.yamlundersecrets, mapped to a CI secret name.
Ports
defaultPortslists only what players connect to (game and query ports). RCON, REST and admin ports are never published.- Use
"7777/udp"or"8211:8211/udp". Every port needs its protocol. - Nodes always publish these container ports (only the host side moves when servers share a node).
So the game's own port is not a setting: write it into the startup command or config file. A
game that must announce the port players use gets
{PUBLIC_PORT_<port>}.
The container user
- Run
gbrgames inspect-image -game <id>. If the image is built around its own user (it declaresUSER, or locks a home folder to a uid other than 1000), setcontainerUserto thatuid:gid, as the inspector reports it.
The startup command
- Read settings under the names the agent gives them (see above).
${RCON_PORT}when the setting isRCONPortsilently ignores the owner's value; it arrives asRCONPORT. - Use
${VAR:-default}for anything that may be unset. - Only use launch options the game's documentation lists, spelled as it spells them. Servers ignore options they don't know, so an invented one boots fine and its setting does nothing.
- Quote text settings where they're used:
-name "${SERVER_NAME}".ARGS="-name ${SERVER_NAME}"; ./server $ARGSsplits "GBRHosting - My Server" into words; build optional arguments in a bash array instead (args=(-name "$SERVER_NAME"); [ -n "$PASSWORD" ] && args+=(-password "$PASSWORD"); ./server "${args[@]}"). - If the image installs the game itself on start, make sure a failed install is retried rather than
starting a server that isn't there: log in to SteamCMD once (
+login anonymous +quit) before the install, and retry the install a few times until the server's executable exists.
Memory
requiredMemoryMbis the container's hard limit. A JVM or engine heap must stay about 1 GB below it: a3Gheap for4096MB. A heap equal to the limit is killed on start.
Settings (defaultConfig and configSchema)
- Take keys and default values from what the server really writes: run
gbrgames first-boot -game <id>and read the settings in its report. - Every key in
defaultConfighas aconfigSchemaproperty, and the other way round. - Every property has a
title, a shortdescriptionfor a player, atype, adefaultequal to thedefaultConfigvalue, and acategory: one ofSecurity & Access,Gameplay & Rules,World & Environment,System & General. Mark the few that matter when deploying with"showOnDeploy": true. - An EULA or other agreement is a boolean with
"x-consent": {"label": "...", "url": "..."}and defaultfalse. Nevertrue; the owner accepts it themselves. - Keys may only use letters, digits,
_,-and.. - Placeholders the panel fills in:
{SERVER_NAME},{PUBLIC_IP},{PUBLIC_HOST},{PUBLIC_PORT_<port>},{saved:KEY}. Server names default toGBRHosting - {SERVER_NAME}.
Being ready
readyPattern: a regex that matches the line the server prints once players can join. Take it from a first boot (its report suggests one), and put that real line intest.yamlasreadyExample; validation checks the pattern matches it.failPatterns: lines that mean the server will never come up (e.g. an unaccepted EULA).- No reliable ready line: leave
readyPatternout, and the boot test checks the server stays up (settleSecintest.yaml, default 120 s). - After its ready line the server must stay up for
holdSec(default 60 s), and then listen on every port indefaultPorts. A server that binds other ports, or only127.0.0.1, fails: players can't reach it.
test.yaml
config: only what the server can't start without in CI (accepting the EULA, for example).secrets: required values that can't be public, mapped to CI secret names.timeoutSeccovers install plus start; SteamCMD games need 2400 or more.
Commands and players (agnosticCommandConfig)
- Kick, ban, save and broadcast go through RCON or REST using the server's own password key
(
passwordKey/rconPasswordKeypointing at thex-generatesetting). If a game has neither, mark the action"type": "UNSUPPORTED"; never invent one. playerJoinRegex/playerLeaveRegexcapture the player name from real log lines.
Before opening a PR
go run ./cmd/gbrgames validate
go run ./cmd/gbrgames inspect-image -game <id>
go run ./cmd/gbrgames first-boot -game <id> # on a machine with Docker and the gbr-boottest network
CI then boot-tests the changed game with the node-agent's real deploy code before anything reaches the panel.