From 6981e2fc8cef8f0a96d9eed63c58f7fa3695df72 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 23 Aug 2026 18:40:24 +0200 Subject: [PATCH 1/4] docs(velocity): document static server discovery --- deploy/containers/velocity.mdx | 22 ++++++- reference/plugins/agones/index.mdx | 11 ++++ reference/plugins/agones/velocity.mdx | 90 +++++++++++++++++++++------ 3 files changed, 102 insertions(+), 21 deletions(-) diff --git a/deploy/containers/velocity.mdx b/deploy/containers/velocity.mdx index df1a5ad..0043427 100644 --- a/deploy/containers/velocity.mdx +++ b/deploy/containers/velocity.mdx @@ -31,6 +31,26 @@ FROM ghcr.io/groundsgg/containers/velocity:latest Verify that the image is available locally with `docker image inspect ghcr.io/groundsgg/containers/velocity:latest`. +## Static Backends + +The image bundles `plugin-agones`, which can register non-Agones backends through +`GROUNDS_STATIC_SERVERS`. Provide a comma-separated list of `name=host:port` entries: + +```yaml +env: + - name: GROUNDS_STATIC_SERVERS + value: "buildserver=buildserver:25565" +``` + +Every proxy that players can reach needs the same entry. After the proxies restart, permitted +players can connect with `/server buildserver`. + + +The backend must use modern Velocity forwarding and the same forwarding secret as the proxies. +Static discovery does not configure Paper or make an offline-mode backend safe to expose publicly. + + ## Documentation -For more details, see the [velocity directory](https://github.com/groundsgg/containers/tree/main/velocity) in the repository. +- Configure discovery and forwarding with the [Velocity plugin reference](/reference/plugins/agones/velocity#static-servers). +- Review the [velocity directory](https://github.com/groundsgg/containers/tree/main/velocity) in the container repository. diff --git a/reference/plugins/agones/index.mdx b/reference/plugins/agones/index.mdx index 7303bb0..0782871 100644 --- a/reference/plugins/agones/index.mdx +++ b/reference/plugins/agones/index.mdx @@ -22,6 +22,7 @@ count: On Velocity, the plugin adds two responsibilities on top of proxy-side state sync: - it polls the Kubernetes API for running gameservers and registers them with the proxy +- it registers explicitly configured non-Agones backends from `GROUNDS_STATIC_SERVERS` - it routes newly joining players to a lobby server and rejects logins when no lobby is available @@ -69,6 +70,15 @@ flowchart LR `grounds/server-type=lobby` must exist for new proxy logins to succeed. +## Static Backends + +Use `GROUNDS_STATIC_SERVERS` for a backend that does not run as an Agones `GameServer`, such as a +temporary buildserver deployed as a regular Kubernetes `Deployment`. Static servers join the +Velocity registry without participating in Agones lifecycle management or lobby selection. + +Continue with [Velocity](/reference/plugins/agones/velocity#static-servers) for the configuration +format, validation rules, and forwarding requirements. + ## Choose Your Platform @@ -93,6 +103,7 @@ flowchart LR Use this plugin when: - your gameserver deploys into the Grounds Agones-managed cluster +- a regular backend must be reachable through the same Velocity proxies - you want Agones state to reflect real player activity instead of a static allocation call - your gamemode publishes its role through `grounds/server-type` so the Velocity proxy can route to it diff --git a/reference/plugins/agones/velocity.mdx b/reference/plugins/agones/velocity.mdx index d50436c..8e96cf7 100644 --- a/reference/plugins/agones/velocity.mdx +++ b/reference/plugins/agones/velocity.mdx @@ -1,34 +1,41 @@ --- title: "Velocity" -description: "Proxy-side Agones gameserver discovery, lobby routing, and operator command" +description: "Proxy-side Agones and static server discovery, lobby routing, and operator command" --- Use the Velocity module of `plugin-agones` when you run the Grounds proxy in front of -Agones-managed gameservers. The plugin replaces the static `velocity.toml` server list with live -Kubernetes-driven discovery and routes joining players to a lobby. +Agones-managed gameservers. The plugin combines Kubernetes-driven discovery with optional static +backends and routes joining players to a lobby. ## Requirements -The plugin assumes the following environment: +For Agones discovery, the plugin expects the following environment: -- the Velocity pod runs in the `games` namespace alongside its gameservers -- the pod has read access to the Agones `gameservers.agones.dev` custom resources (`list` verb) -- an Agones sidecar runs next to the Velocity container and exposes the SDK on - `http://localhost:9358` +- `GROUNDS_AGONES_NAMESPACE` or `POD_NAMESPACE` identifies the namespace to query; the fallback is + `games` +- the pod can list Agones `gameservers.agones.dev` resources in that namespace +- the pod can read gameserver pods when a `GameServer` status does not contain the configured + address type -The plugin unregisters every server defined in `velocity.toml` during startup. Your `velocity.toml` -`[servers]` section is effectively ignored once the plugin is loaded. +The Grounds Velocity image contains three example backends: `lobby`, `factions`, and `minigames`. +The plugin removes only those exact loopback placeholders during startup. It preserves every other +Velocity registration. +The proxy needs an Agones SDK sidecar only when the proxy itself runs as an Agones `GameServer`. +Without the sidecar, backend discovery and routing still work; only proxy self-state management is +disabled. + ## What the Plugin Provides -Once the plugin starts, it runs two responsibilities in parallel on the proxy: +Once the plugin starts, it runs these responsibilities on the proxy: -- `GameServerStateManager` syncs the proxy's own Agones state (`Ready` or `Allocated`) with the - connected player count +- `DiscoveryService` registers configured static backends - `DiscoveryService` polls Kubernetes every two seconds and keeps Velocity's server registry in sync with the running gameservers +- `GameServerStateManager` syncs the proxy's own Agones state (`Ready` or `Allocated`) with the + connected player count when an SDK sidecar is present Every discovered gameserver is registered under its Kubernetes resource name. The server's role is read from the `grounds/server-type` label and cached for routing and the `/agones` command. @@ -74,6 +81,42 @@ If your gameserver follows all four steps, it appears in `/agones` on the proxy of reaching a running state. +## Static Servers + +Use a static server when a backend runs as a regular Kubernetes `Deployment` or outside Agones. +Set `GROUNDS_STATIC_SERVERS` on every Velocity proxy that must expose it: + +```yaml +env: + - name: GROUNDS_STATIC_SERVERS + value: "buildserver=buildserver:25565" +``` + +The value is a comma-separated list of `name=host:port` entries: + +```text +buildserver=buildserver:25565,events=events.stage.svc.cluster.local:25566 +``` + +The plugin trims surrounding whitespace and validates the complete value at startup. Each entry +must have a non-empty name and host, and its port must be between `1` and `65535`. Server names are +case-insensitive; duplicates such as `BuildServer` and `buildserver` fail configuration. + +Static servers have the role `static`. They appear in Velocity's `/server` command and in +`/agones`, but they are never selected as login lobbies. If a static server and an Agones +`GameServer` use the same name, the static server takes precedence. + + +Static registration configures routing only. A Paper backend behind Velocity must also use modern +Velocity forwarding with the same forwarding secret as the proxies. Do not expose an offline-mode +backend through a public `LoadBalancer`. + + + +After all proxies restart, use `/server buildserver` to connect to the example backend. The player +still needs Velocity's `velocity.command.server` permission. + + ## Lobby Routing The plugin treats `grounds/server-type=lobby` as the entry point for new connections. @@ -107,7 +150,8 @@ The plugin registers an operator command for inspecting the current proxy state. | Output scope | all registered proxy servers | The command prints every registered server with its role, address, and connected player count. -Roles are color-coded: `lobby` (green), `game` (aqua), `match` (light purple), `unknown` (gray). +Roles include `lobby`, `game`, `match`, and `static`; registrations without a known role appear as +`unknown`. Example output: @@ -133,10 +177,9 @@ issue redundant calls to the sidecar. -If the plugin cannot build a Kubernetes client at startup, discovery stays disabled for the -process lifetime. The plugin logs the reason and continues without unregistering configured -servers, registering lobby-routing listeners, or polling Kubernetes. Existing `velocity.toml` -servers remain registered until the proxy restarts with a working Kubernetes configuration. +If the plugin cannot build a Kubernetes client at startup, Agones discovery stays disabled for the +process lifetime. The plugin still removes the exact image placeholders, registers +`GROUNDS_STATIC_SERVERS`, and installs the routing listeners. @@ -145,8 +188,15 @@ routing continue to work because they depend on the Kubernetes API, not on the s -Gameservers without a `PodIP` entry in `status.addresses` are skipped and logged as -`missing_pod_ip`. This usually clears up on the next poll once Agones populates the status. +If the configured address type is missing from `GameServer.status.addresses`, the plugin reads the +same-named pod and uses its pod IP. If neither source provides an address, the server is skipped and +retried during the next poll. + + + +The plugin rejects malformed entries and duplicate names during initialization. Correct +`GROUNDS_STATIC_SERVERS` and restart the proxy. An absent or blank value disables static +registration. From 17ca0f4ea617c2f87be754eb0123ad9d66e6be0c Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 23 Aug 2026 19:16:56 +0200 Subject: [PATCH 2/4] docs(velocity): document shared static server config --- deploy/containers/velocity.mdx | 26 +++++++++++-- reference/plugins/agones/index.mdx | 7 +++- reference/plugins/agones/velocity.mdx | 53 +++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/deploy/containers/velocity.mdx b/deploy/containers/velocity.mdx index 0043427..df16879 100644 --- a/deploy/containers/velocity.mdx +++ b/deploy/containers/velocity.mdx @@ -34,7 +34,26 @@ Verify that the image is available locally with `docker image inspect ghcr.io/gr ## Static Backends The image bundles `plugin-agones`, which can register non-Agones backends through -`GROUNDS_STATIC_SERVERS`. Provide a comma-separated list of `name=host:port` entries: +`GROUNDS_STATIC_SERVERS`. In production, keep this value in one dedicated Helm chart as an +immutable, versioned ConfigMap. Every Java Velocity proxy must source the same required key: + +```yaml +env: + - name: GROUNDS_STATIC_SERVERS + valueFrom: + configMapKeyRef: + name: velocity-static-servers-v1 + key: GROUNDS_STATIC_SERVERS +``` + +The chart owns a ConfigMap such as `velocity-static-servers-v1`, with `immutable: true`, the +`GROUNDS_STATIC_SERVERS` key, and the Helm annotation `helm.sh/resource-policy: keep`. Its value +is a comma-separated list of `name=host:port` entries. To change the list, create a new versioned +ConfigMap, update every proxy reference, and roll out the proxies. Keep the prior version until +that rollout finishes, then garbage-collect it deliberately. Do not duplicate a literal value in +each production proxy chart. + +For local development, a literal environment value remains convenient: ```yaml env: @@ -42,8 +61,9 @@ env: value: "buildserver=buildserver:25565" ``` -Every proxy that players can reach needs the same entry. After the proxies restart, permitted -players can connect with `/server buildserver`. +After the proxies restart, permitted players can connect with `/server buildserver`. See the +[Velocity plugin reference](/reference/plugins/agones/velocity#static-servers) for the parser, +startup failure behavior, and forwarding requirements. The backend must use modern Velocity forwarding and the same forwarding secret as the proxies. diff --git a/reference/plugins/agones/index.mdx b/reference/plugins/agones/index.mdx index 0782871..227fc8b 100644 --- a/reference/plugins/agones/index.mdx +++ b/reference/plugins/agones/index.mdx @@ -74,10 +74,13 @@ flowchart LR Use `GROUNDS_STATIC_SERVERS` for a backend that does not run as an Agones `GameServer`, such as a temporary buildserver deployed as a regular Kubernetes `Deployment`. Static servers join the -Velocity registry without participating in Agones lifecycle management or lobby selection. +Velocity registry without participating in Agones lifecycle management or lobby selection. In +production, every Java Velocity proxy reads the same required value from the dedicated immutable, +versioned static-server ConfigMap; the plugin loads and caches it at startup. Local development +may provide the value directly as an environment literal. Continue with [Velocity](/reference/plugins/agones/velocity#static-servers) for the configuration -format, validation rules, and forwarding requirements. +format, rollout and validation rules, and forwarding requirements. ## Choose Your Platform diff --git a/reference/plugins/agones/velocity.mdx b/reference/plugins/agones/velocity.mdx index 8e96cf7..0d996bb 100644 --- a/reference/plugins/agones/velocity.mdx +++ b/reference/plugins/agones/velocity.mdx @@ -84,15 +84,40 @@ of reaching a running state. ## Static Servers Use a static server when a backend runs as a regular Kubernetes `Deployment` or outside Agones. -Set `GROUNDS_STATIC_SERVERS` on every Velocity proxy that must expose it: +In production, a dedicated Helm chart owns one immutable, versioned ConfigMap for every Java +Velocity proxy. The ConfigMap is retained with `helm.sh/resource-policy: keep`, allowing the old +and new versions to overlap during a rollout: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: velocity-static-servers-v1 + annotations: + helm.sh/resource-policy: keep +immutable: true +data: + GROUNDS_STATIC_SERVERS: "buildserver=buildserver:25565" +``` + +Each proxy must use a mandatory reference to that same key: ```yaml env: - name: GROUNDS_STATIC_SERVERS - value: "buildserver=buildserver:25565" + valueFrom: + configMapKeyRef: + name: velocity-static-servers-v1 + key: GROUNDS_STATIC_SERVERS ``` -The value is a comma-separated list of `name=host:port` entries: +The plugin reads and caches the value at startup. To change it, create a new ConfigMap name (for +example, `velocity-static-servers-v2`), update every proxy reference, and roll out all proxies. +Only garbage-collect the retained old version after the rollout completes. Do not set independent +literal values on production proxies. + +For local development, set `GROUNDS_STATIC_SERVERS` directly. The value is a comma-separated list +of `name=host:port` entries: ```text buildserver=buildserver:25565,events=events.stage.svc.cluster.local:25566 @@ -117,6 +142,22 @@ After all proxies restart, use `/server buildserver` to connect to the example b still needs Velocity's `velocity.command.server` permission. +## Drain Routing + +During an automatic drain, the proxy defers a move only for players on a backend whose current +role is `game` or `match`. Players on `static`, `lobby`, an unknown role, or a backend with no role +are moved immediately. + +For an automatically drained Java player who is currently on a static backend, the source proxy +writes a short-lived, namespaced Minecraft transfer cookie containing that backend name. The target +proxy waits for up to one second, consumes the cookie, and uses the named backend directly only if +it is still registered with the `static` role. A missing, expired, invalid, or unknown cookie falls +back to normal lobby routing. This requires Java 1.20.5 or later, matching the existing +host-transfer minimum. Bedrock drain remains disabled. + +The preservation applies only to automatic drain. Generic `/region` moves and other manual proxy +moves do not preserve the current backend. + ## Lobby Routing The plugin treats `grounds/server-type=lobby` as the entry point for new connections. @@ -199,6 +240,12 @@ The plugin rejects malformed entries and duplicate names during initialization. registration. + +In production the environment reference is mandatory, so a missing ConfigMap or key prevents the +proxy pod from starting. Restore the referenced version or update all proxy references to a valid +versioned ConfigMap, then roll out the affected proxies. + + Unlabeled gameservers are silently skipped. Check the `GameServer` resource if you expect it to appear and it does not. From d241536587abc4a28764dab0f8633304b47f0ffd Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 23 Aug 2026 19:31:16 +0200 Subject: [PATCH 3/4] docs(velocity): clarify static drain routing --- reference/plugins/agones/index.mdx | 4 ++-- reference/plugins/agones/velocity.mdx | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/reference/plugins/agones/index.mdx b/reference/plugins/agones/index.mdx index 227fc8b..efbfb85 100644 --- a/reference/plugins/agones/index.mdx +++ b/reference/plugins/agones/index.mdx @@ -19,7 +19,7 @@ count: - the last player leaves → the runtime calls `Ready` - a 10 second fallback loop reconciles the state if an event is missed -On Velocity, the plugin adds two responsibilities on top of proxy-side state sync: +On Velocity, the plugin adds three responsibilities on top of proxy-side state sync: - it polls the Kubernetes API for running gameservers and registers them with the proxy - it registers explicitly configured non-Agones backends from `GROUNDS_STATIC_SERVERS` @@ -47,7 +47,7 @@ runtime. The Velocity discovery path depends on three conventions that your gameserver deployment must follow: -- the pod runs in the `games` Kubernetes namespace +- the pod runs in the configured discovery namespace (default: `games`) - the Agones `GameServer` carries the label `grounds/server-type` with one of `lobby`, `game`, or `match` - the pod exposes Minecraft on port `25565` and reports its `PodIP` under `status.addresses` diff --git a/reference/plugins/agones/velocity.mdx b/reference/plugins/agones/velocity.mdx index 0d996bb..978a705 100644 --- a/reference/plugins/agones/velocity.mdx +++ b/reference/plugins/agones/velocity.mdx @@ -45,9 +45,9 @@ read from the `grounds/server-type` label and cached for routing and the `/agone For a gameserver pod to appear in Velocity, it must satisfy the discovery contract: - -The discovery query is scoped to the `games` namespace. Gameservers outside that namespace are -ignored. + +The discovery query is scoped to `GROUNDS_AGONES_NAMESPACE` or `POD_NAMESPACE`; if neither is set, +it defaults to `games`. Gameservers outside that namespace are ignored. @@ -149,11 +149,13 @@ role is `game` or `match`. Players on `static`, `lobby`, an unknown role, or a b are moved immediately. For an automatically drained Java player who is currently on a static backend, the source proxy -writes a short-lived, namespaced Minecraft transfer cookie containing that backend name. The target -proxy waits for up to one second, consumes the cookie, and uses the named backend directly only if -it is still registered with the `static` role. A missing, expired, invalid, or unknown cookie falls -back to normal lobby routing. This requires Java 1.20.5 or later, matching the existing -host-transfer minimum. Bedrock drain remains disabled. +signs a short-lived, namespaced Minecraft transfer cookie with HMAC using the existing shared +Velocity forwarding secret. It requests and receives an exact client echo of that cookie before it +initiates the transfer. The target proxy validates the cookie signature and TTL, then uses the named +backend directly only if it is still registered with the `static` role. If the echo does not arrive +within one second, or the cookie is missing, expired, invalid, or names an unknown backend, the +transfer falls back to normal routing and therefore to a lobby when one is available. This requires +Java 1.20.5 or later, matching the existing host-transfer minimum. Bedrock drain remains disabled. The preservation applies only to automatic drain. Generic `/region` moves and other manual proxy moves do not preserve the current backend. From 73a9ba72364ed6f4eaaf7ed88f28fc76f5bc942a Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 23 Aug 2026 19:47:17 +0200 Subject: [PATCH 4/4] docs(velocity): document gameserver address selection --- reference/plugins/agones/velocity.mdx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/reference/plugins/agones/velocity.mdx b/reference/plugins/agones/velocity.mdx index 978a705..d842b35 100644 --- a/reference/plugins/agones/velocity.mdx +++ b/reference/plugins/agones/velocity.mdx @@ -13,6 +13,10 @@ For Agones discovery, the plugin expects the following environment: - `GROUNDS_AGONES_NAMESPACE` or `POD_NAMESPACE` identifies the namespace to query; the fallback is `games` +- `GROUNDS_AGONES_ADDRESS_TYPE` must exactly match a type reported in + `GameServer.status.addresses`; common values are `PodIP`, `ExternalIP`, `InternalIP`, and + `Hostname`, with `PodIP` as the default +- `GROUNDS_AGONES_PORT` selects the backend TCP port; the default is `25565` - the pod can list Agones `gameservers.agones.dev` resources in that namespace - the pod can read gameserver pods when a `GameServer` status does not contain the configured address type @@ -52,7 +56,8 @@ it defaults to `games`. Gameservers outside that namespace are ignored. Set `grounds/server-type` to `lobby`, `game`, or `match` on the `GameServer` resource metadata. -Unlabeled gameservers are skipped. +Unlabeled gameservers are skipped. This example uses the fallback namespace `games`; set +`metadata.namespace` to the configured discovery namespace when it differs. ```yaml apiVersion: agones.dev/v1 @@ -65,9 +70,12 @@ metadata: ``` - -The proxy registers the pod at `PodIP:25565`. The plugin does not honor Agones allocated ports — -the gameserver must listen on the default Minecraft port inside the pod. + +The proxy reads the address whose type matches `GROUNDS_AGONES_ADDRESS_TYPE` and connects to +`GROUNDS_AGONES_PORT`. These default to `PodIP` and `25565`. If the selected address is missing from +`GameServer.status.addresses`, the plugin falls back to the gameserver pod IP when Kubernetes +exposes one; otherwise the gameserver is skipped. The plugin does not honor Agones allocated ports, +so the gameserver must listen on the configured fixed port inside the pod.