Files
Felis/deploy/limbo
Lemon-miaow fa0e8d7d97 docs(troubleshooting): 8e/9/13b/15 — registry-hosted images and the loopback pull path
- §8e: the executor-mirror recipe now pushes into the internal registry (the
  node's 127.0.0.1:5000, or a kubectl port-forward from another machine)
  instead of advising bare node-containerd imports — GC collects those and an
  air-gapped box cannot restore them.
- §13b: after an image GC the images come back on their own (registry + the
  registries.yaml mirror); keeps the operator checks (registry pod, mirror
  file, re-mirror a tag) and the old fallback for unmirrored images.
- §15: rollout undo no longer needs a manual re-import for installer-built tags.
- §9: documents the loopback hostPort/mirror pair as one unit and the 2Gi
  registry memory floor (audit #46).
- deploy/{limbo,lobby}/README: manual image builds publish into the registry and
  point felis.toml at the registry ref.
2026-09-23 19:03:11 +08:00
..

Login-limbo image (LOOHP/Limbo + felis-limbo)

The always-on login auth gate. felis setup provisions it as a system service (DesiredState=Running, reaper-exempt) when [velocity] login_image is set in felis.toml. Every fresh connection lands here first; it is the only safe fallback (a stopped/starting backend routes here, never past authentication).

Code-only. This image is not built by the Go CI. It compiles the plugins/limbo plugin (plus the shared plugins/shared link core it srcDir-includes) and bundles it with a LOOHP/Limbo release.

What the plugin does

felis-limbo does two jobs — readiness and the in-game login flow.

Readiness

LOOHP/Limbo has no RCON, so Felis cannot use its usual RCON readiness probe (spec §5). A bare TCP check would report "ready" the instant the socket binds. The plugin instead serves an HTTP readiness endpoint that flips to 200 only after the first server tick — i.e. once the limbo has genuinely started. The login MinecraftServer sets spec.startup.healthHTTPPort: 8080, so the pod's HTTP readinessProbe (and, with RCON disabled, the operator's Ready gate) follows that true signal.

  • Endpoint: GET /healthz on :8080 (override with FELIS_HEALTH_PORT).
  • 503 starting before the first tick, 200 ok after.
  • Fail-closed: if the endpoint cannot bind, readiness never turns green and the operator keeps the gate in Starting — it never advertises an unstarted gate.

In-game login (spec §B3)

A player reaching the limbo has been UUID-verified upstream (Velocity online-mode) but is not yet linked to a web account. On join, off the tick thread, the plugin:

  1. checks the username-collision blacklist and disconnects a barred squatter UUID (the genuine Mojang player — different UUID — passes);
  2. mints a one-time Bind Code for the verified UUID via the felis-api internal face;
  3. opens a book with a clickable link to console.<root_domain> plus a chat line carrying the code, and tells the player to finish in their system browser — never the WeChat/QQ in-app browser, where passkey/WebAuthn does not work (the web entry additionally guards this; see internal/panel);
  4. polls link/status/{uuid} until the player redeems the code on the web console, then transfers them to the lobby via a BungeeCord Connect plugin message on bungeecord:main;
  5. disconnects (fail-closed) on blacklist, on a mint/transport failure, or when the login window elapses — "rather refuse than admit unauthenticated".

Configuration (deployment inputs, never compiled in; env wins over a felis-link.properties template written in the plugin data dir on first run):

Env Meaning Default
FELIS_API_BASE_URL felis-api internal face base URL (required for login)
FELIS_SERVICE_TOKEN internal service token (secret) (required for login)
FELIS_ROOT_DOMAIN deployment zone, builds https://console.<zone> (required for login)
FELIS_LOBBY_SERVER Velocity server name to transfer to lobby
FELIS_LOGIN_TIMEOUT_SECONDS login window (clamped 30–3600) 600
FELIS_HEALTH_PORT readiness port 8080

If the API config or the root domain is absent the login flow stays OFF and the plugin runs readiness-only (the same "load un-crippled" fail-safe the other Felis plugins use), so a bare image still boots — production must supply the config for the gate to authenticate. Transfer requires Velocity to accept the BungeeCord plugin-message channel (bungee-plugin-message-channel on the proxy).

Build

LOOHP/Limbo has no official image and no release zip. Its CI (ci.loohpjames.com/job/Limbo) publishes two loose artifacts per build — target/Limbo-<ver>.jar and spawn.schem — so the image is assembled from those two URLs (there is no bundled server.properties; Limbo writes a default on first run):

docker build -f deploy/limbo/Dockerfile \
  --build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
  --build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
  --build-arg LIMBO_VERSION=<maven-api-version> \
  -t felis-limbo:demo .
  • LIMBO_JAR_URL (required) — the server jar; it is saved as Limbo.jar.
  • LIMBO_SCHEM_URL (optional) — the default spawn schematic, saved as spawn.schem and loaded as the spawn world.
  • LIMBO_VERSION — the Limbo maven API version the plugin compiles against (Gradle -PlimboVersion). This differs from the jar's CI build-qualified filename: e.g. the jar Limbo-2026.0.2-ALPHA-26.2.jar corresponds to maven version 2026.0.2-ALPHA (the -26.2 CI qualifier is not published to the maven repo).

Publish it into the cluster's registry and point config at it. On the node itself (docker treats 127.0.0.1 as insecure by default):

docker tag  felis-limbo:demo 127.0.0.1:5000/felis/limbo:demo
docker push 127.0.0.1:5000/felis/limbo:demo
# felis.toml → [velocity] login_image = "registry.felis.svc:5000/felis/limbo:demo"
sudo felis setup

The registry keys a repository by the path after the host, so pushing through a kubectl -n felis port-forward svc/registry 5000:5000 from another machine is equivalent. Hosting the image in the registry (rather than only importing it into containerd) is what lets kubelet re-pull it after an image GC.

Ports (handled for you)

The entrypoint (deploy/limbo/entrypoint.sh) pins Limbo's server-port to FELIS_GAME_PORT (default 25565, the operator's GamePort) on every start — LOOHP/Limbo would otherwise default to 30000, unreachable through the Velocity NetworkPolicy / Service / probe the operator drives off that one const. It is idempotent, so a persisted world volume keeps all its other server.properties settings. Do not override FELIS_GAME_PORT except in lockstep with the operator.

Configure (deployer's responsibility)

One setting this image does not guess (it keeps the release's own default):

  • Player forwarding — align Limbo's forwarding with the off-cluster Velocity proxy so authenticated players hand off cleanly, and enable the BungeeCord plugin-message channel on the proxy so the login gate's Connect transfer to the lobby lands.

The login flow's own inputs (FELIS_API_BASE_URL, FELIS_ROOT_DOMAIN, FELIS_LOBBY_SERVER, FELIS_SERVICE_TOKEN; see In-game login above) are wired in for you — you do not set them by hand:

  • The three non-secret vars are baked into the login MinecraftServer's spec.env by felis setup (cmd/felis derives the internal API URL from the control namespace — the platform default felis; a renamed control namespace must be reflected by hand — and the root domain from felis.toml).
  • FELIS_SERVICE_TOKEN is a secret, so it is never written into the CRD. felis setup replicates the felis-service-token Secret from the control namespace into the minecraft namespace, and the operator injects it into the login pod (only) via a secretKeyRef, keyed off the reserved login name. Until the token is present the plugin fail-safes to readiness-only, so the gate is never broken — it simply does not authenticate yet.
  • Service: the login pod dials FELIS_API_BASE_URL, which resolves to the ClusterIP Service felis-api-internal (control namespace) that fronts the api pod's internal port 8081. That Service is deliberately separate from the external NodePort felis-api (443) so the no-Zero-Trust internal face is never published on a node's external IP.
  • NetworkPolicy: none is required today — neither the minecraft-namespace egress nor the control-namespace ingress is policy-locked, so the login pod's call to the API internal port is reachable. If a future deployment adds a minecraft egress lock or a control-namespace ingress fence, it must also open the login-pod → felis-api-internal (8081) path.

The Velocity gate/lobby wiring is printed by felis setup and enforces the invariant: fresh connections hit login first, and only an authenticated release from that gate can enter the post-auth lobby or a remembered user backend.