Files
Spinner/README.md
T
VeillaxandClaude Sonnet 5.5 b53af0e196
CI / test (push) Successful in 2m32s
Add Spinner, a Minecraft hostname-routing reverse proxy
Routes Java Edition connections by the hostname in the handshake, with
wildcard domains, backend failover, PROXY protocol v2, cached server-list
status from health probes, hot-reloaded config, and an admin page and
metrics endpoint. Includes CI, tagged releases and a container image.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 19:40:40 -06:00

4.1 KiB

Spinner

A small Minecraft Java reverse proxy. One public port, many backend servers, chosen by the hostname the player typed (survival.example.com and creative.example.com can share an IP and port). Spinner only reads the handshake, then pipes bytes, so it never touches the encrypted session and works with online-mode servers, mods and any client version.

go build -o spinner ./cmd/spinner
cp spinner.example.json spinner.json
cp .env.example .env        # set SPINNER_ADMIN_TOKEN
./spinner -config spinner.json

What it does

Situation Server list Login
Host matches a server, backend up cached real MOTD, version and player count (refreshed every probe_interval_secs) proxied
Host matches, backend unreachable that server's offline_motd disconnect with offline_motd
Host matches no server unknown_motd disconnect with unknown_motd
Server at max_connections, or IP at max_conns_per_ip Server is full / closed Server is full / closed
  • Routing: exact domains win; *.example.com matches any subdomain depth but not the apex; longer wildcards win. Matching ignores case, a trailing dot and Forge's \0FML\0 suffix.
  • Failover: a server may list several addresses; they are tried in order.
  • PROXY protocol v2: set send_proxy_protocol per server so the backend sees real player IPs (the backend must be configured to expect it).
  • Hot reload: edit the config file (or use the admin UI) and it applies within 2 seconds. Invalid edits are logged and ignored. bind and admin_bind need a restart.

Config

JSON, see spinner.example.json. Top level: bind, admin_bind (empty disables the admin), handshake_timeout_secs, connect_timeout_secs, probe_interval_secs, max_conns_per_ip, unknown_motd, servers. Per server: name, domains, addresses, send_proxy_protocol, offline_motd, max_connections. MOTDs accept § colour codes and \n.

Admin

With admin_bind and SPINNER_ADMIN_TOKEN set, http://<admin_bind>/ shows servers, health, connections and traffic, and edits the config. Bind it to localhost or a private network and put TLS in front if it must be public.

Endpoint
GET /api/overview totals plus per-server health and stats
GET/PUT /api/config read or replace the config (validated, written atomically)
GET /metrics Prometheus text format

All except / need Authorization: Bearer <token>.

Docker

cp .env.example .env        # set SPINNER_ADMIN_TOKEN
docker compose -f deploy/docker-compose.yml up -d

The image is git.veillax.com/veillax/spinner (linux/amd64 and linux/arm64). It keeps its config in the /data volume and writes a starter spinner.json there on first start, so the admin page on port 8080 is the quickest way to add servers. Keep that port private or behind HTTPS.

Releases

Push a tag to build archives, publish a release and push the image:

git tag -a v0.1.0 -m "spinner v0.1.0" && git push origin v0.1.0

.gitea/workflows/release.yml runs deploy/build.sh, uploads the archives with deploy/publish-release.sh, then builds and pushes the image. Set the repository variable PUBLISH_DOCKER to false to skip the image. If the job token cannot write releases or packages, add the secrets RELEASE_TOKEN and REGISTRY_TOKEN (and the variable REGISTRY_USER).

To build by hand: sh deploy/build.sh puts binaries and archives in dist/, then docker buildx build . packages the linux binary.

Not in v1

Bedrock Edition, legacy (pre-1.7) pings, up/down webhooks, self-update and WireGuard backends (Reef has selfupdate and wg ready for these), and ending player sessions on config reload (existing connections keep running until they close).

Development

go vet ./... && go test ./...
node scripts/lint-design.mjs

Reef is fetched from the server as git.veillax.com/Veillax/reef; set GOPRIVATE=git.veillax.com if it is private. To work against a local Reef checkout, use a go.work file (ignored by git). The admin page follows the Lightwell design language, see docs/design.md. Licence: AGPL-3.0.