Pushes :edge for main and :x.y.z, :x.y and :latest for release tags, optionally mirrored to another registry. The release workflow no longer builds the image itself, so a tag does not publish it twice. The README covers running the image anywhere and the settings the workflows read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
114 lines
5.4 KiB
Markdown
114 lines
5.4 KiB
Markdown
# 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](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
|
|
|
|
The image is `git.veillax.com/veillax/spinner` for linux/amd64 and linux/arm64. It runs as a non-root user, keeps its config in the `/data` volume, and writes a starter `spinner.json` there on first start. The admin page on port 8080 is the quickest way to add servers; keep it private or behind HTTPS.
|
|
|
|
| Tag | Meaning |
|
|
| --- | --- |
|
|
| `latest` | The newest release |
|
|
| `1.2.3`, `1.2` | An exact release, or the newest patch of a minor version |
|
|
| `edge` | The latest commit on `main` |
|
|
| `sha-<commit>` | One specific commit |
|
|
|
|
Run it anywhere Docker runs:
|
|
|
|
```
|
|
docker run -d --name spinner --restart unless-stopped \
|
|
-p 25565:25565 -p 127.0.0.1:8080:8080 \
|
|
-e SPINNER_ADMIN_TOKEN=change-me \
|
|
-v spinner-data:/data \
|
|
git.veillax.com/veillax/spinner:latest
|
|
```
|
|
|
|
Or with Compose: `cp .env.example .env`, set `SPINNER_ADMIN_TOKEN`, then `docker compose -f deploy/docker-compose.yml up -d`. Backends on the Docker host are reachable at `host.docker.internal` (add `--add-host host.docker.internal:host-gateway` on Linux), or put Spinner and the Minecraft containers on one Docker network and use their container names.
|
|
|
|
If the registry is private, run `docker login git.veillax.com` first.
|
|
|
|
## Releases
|
|
|
|
Push a tag to publish a release and its image:
|
|
|
|
```
|
|
git tag -a v0.1.0 -m "spinner v0.1.0" && git push origin v0.1.0
|
|
```
|
|
|
|
Two workflows run on the tag:
|
|
|
|
* `.gitea/workflows/release.yml` builds archives with `deploy/build.sh` and uploads them with `deploy/publish-release.sh`.
|
|
* `.gitea/workflows/docker.yml` tests, builds both Linux binaries and pushes the image. It also pushes `edge` on every push to `main`, and can be run by hand from the Actions tab.
|
|
|
|
Settings, all optional:
|
|
|
|
| Name | Kind | Use |
|
|
| --- | --- | --- |
|
|
| `REGISTRY_TOKEN`, `REGISTRY_USER` | secret, variable | Token with package write permission, if the job token cannot push |
|
|
| `RELEASE_TOKEN` | secret | Token that can write releases, if the job token cannot |
|
|
| `MIRROR_IMAGE`, `MIRROR_USER`, `MIRROR_TOKEN` | variable, variable, secret | Also push to another registry, for example `ghcr.io/you/spinner` or `docker.io/you/spinner` |
|
|
| `PUBLISH_DOCKER` | variable | Set to `false` to skip the image |
|
|
|
|
The runner needs Docker with buildx. To build by hand: `sh deploy/build.sh` puts binaries and archives in `dist/`, then `docker buildx build .` packages the linux binary for your platform.
|
|
|
|
## 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](docs/design.md).
|
|
Licence: AGPL-3.0.
|