Skip to main content
Use the Velocity module of plugin-agones when you run the Grounds proxy in front of Agones-managed gameservers. The plugin combines Kubernetes-driven discovery with optional static backends and routes joining players to a lobby.

Requirements

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
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 these responsibilities on the proxy:
  • 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.

Gameserver Requirements

For a gameserver pod to appear in Velocity, it must satisfy the discovery contract:
1

Deploy into the configured discovery namespace

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.
2

Label the GameServer with a role

Set grounds/server-type to lobby, game, or match on the GameServer resource metadata. Unlabeled gameservers are skipped. This example uses the fallback namespace games; set metadata.namespace to the configured discovery namespace when it differs.
3

Expose Minecraft on the configured port

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.
4

Reach a running Agones state

Only gameservers in Ready, Allocated, or Reserved state are registered. Transitions out of these states unregister the server on the next poll.
If your gameserver follows all four steps, it appears in /agones on the proxy within two seconds of reaching a running state.

Static Servers

Use a static server when a backend runs as a regular Kubernetes Deployment or outside Agones. 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:
Each proxy must use a mandatory reference to that same key:
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:
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.

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 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.

Lobby Routing

The plugin treats grounds/server-type=lobby as the entry point for new connections. If another plugin already sets an initial server (for example through a rejoin feature), the discovery plugin does not override that choice.
“First registered lobby” is a deterministic but unordered pick from Velocity’s internal map. If you need priority routing, plan to pair this with party-aware routing later.

The /agones Command

The plugin registers an operator command for inspecting the current proxy state.
The command prints every registered server with its role, address, and connected player count. Roles include lobby, game, match, and static; registrations without a known role appear as unknown. Example output:

State Sync on the Proxy

The proxy itself runs as an Agones gameserver, so the plugin also manages its own state:
  • the plugin calls Allocate when the first player logs in
  • the plugin calls Ready when the last player disconnects
  • a 10 second fallback loop reconciles the state in case events are missed
The state sync is idempotent: if the proxy is already in the desired state, the plugin does not issue redundant calls to the sidecar.

Failure Modes

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.
The proxy-side state sync logs the failure and keeps trying on its fallback loop. Discovery and routing continue to work because they depend on the Kubernetes API, not on the sidecar.
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.
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.

Next Steps