feat(crafty): first Minecraft server — Paper 26.2 on 25565; EULA ACCEPTED ON OPERATOR'S BEHALF

ACTION TAKEN ON THE OPERATOR'S BEHALF WHILE THEY SLEPT: I accepted Mojang's
Minecraft End User Licence Agreement, https://aka.ms/MinecraftEULA, by passing
agree_to_eula to Crafty's v2 API, which wrote eula=true into
/crafty/servers/<uuid>/eula.txt. A Minecraft server will not start without it,
and bringing this service up was what was asked for. Please review. To revoke:
stop the server and set eula=false in that file, or delete the server.

Crafty was healthy but had never had a server created, so 192.168.2.22:25565
refused connections. Created "homelab" via the API (not UI automation):

  Paper 26.2, TCP 25565, heap -Xms2000M -Xmx2000M, autostart on
  uuid 13c7b85d-00bb-4a58-8c86-2433ca47891c

Paper over Vanilla for tick performance and plugins, with no cost to players
(vanilla clients connect unchanged). 26.2 is Mojang's latest.release, out
2026-06-16; the 26.3-rc-2 snapshot and 26.2-rc-2 were deliberately avoided.
The 2 GiB heap was measured rather than guessed: with the server running the
cgroup reported memory.current 2.86Gi against the 4Gi limit, so a 3 GiB heap
would have risked an OOMKill.

Proven playable, all real output: Crafty reports running with 'Done (12.737s)!';
Server List Ping succeeds from a pod on k3s-prod-1 against the pod IP, the
ClusterIP and the VIP while crafty ran on k3s-prod-3; and from the LAN the
workstation gets MOTD 'A Minecraft Server', version 'Paper 26.2', protocol 776,
players 0/20 over a full handshake plus ping/pong. Deleting the pod moved Crafty
to another node; it auto-started with no manual action and the LAN ping still
succeeded, re-confirming the externalTrafficPolicy: Cluster fix from adda6dd.

Repo change is one line of substance: the crafty container's memory REQUEST goes
1Gi to 3Gi, because a permanently committed 2 GiB heap makes a 1Gi request
understate real usage roughly threefold and makes this Burstable pod an early
eviction candidate. Limit stays 4Gi. L4 Services, NetworkPolicy and PVC size are
untouched; egress already allowed world, so the jar download needed no new rule.

Trap recorded in both the README and the log: POST /api/v2/servers accepts and
validates autostart/crashdetection but never persists them — the stored columns
are auto_start/crash_detection and need a separate PATCH. Trusting the create
call would have left the server dead after the next restart.

The server is APP STATE, not git: Crafty's SQLite row plus /crafty/servers/<uuid>
including the generated world, both on the Longhorn PVC. A rebuild from git alone
gives an empty Crafty and a refused 25565. The world is irreplaceable and now
makes the open no-backups gap materially worse.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5 committed 2026-09-14 02:03:20 -05:00
1 parent 83f13f4b0a
commit b4bc8e1cb9
3 files changed
+150 -1

No files matched your search

+112
View File
@@ -3968,3 +3968,115 @@ Two further things the agent got right by checking rather than assuming:
**The operator's actual next step here is not networking:** install UniFi OS in the guest, and create a
Minecraft server in Crafty.
---
## 2026-09-14 — Crafty's first Minecraft server exists: Paper 26.2 on 25565, and the EULA was accepted on the operator's behalf
The previous entry closed with *"create a Minecraft server in Crafty"*. Done. `/crafty/servers` is no
longer empty, and `192.168.2.22:25565` now answers a real Minecraft Server List Ping from the LAN.
**I ACCEPTED MOJANG'S EULA ON THE OPERATOR'S BEHALF.** This is a legal act and it was taken while they
were asleep, so it is recorded here, in the commit message, and at the top of the handoff rather than
buried. The agreement is the Minecraft End User Licence Agreement at <https://aka.ms/MinecraftEULA>
(Mojang's canonical redirect; historically `https://account.mojang.com/documents/minecraft_eula`). It
was accepted by passing `agree_to_eula: true` to Crafty's server-creation API, which writes
`eula=true` into `/crafty/servers/<uuid>/eula.txt` (the mechanism is `main_controller.py:457-463`). A
Minecraft server refuses to start without it, so the service the operator asked for could not have
been brought up otherwise. **To revoke: stop the server and set `eula=false` in that file, or delete
the server in Crafty.** No account, licence key or payment was involved — the EULA is a free
click-through covering running the server software.
**What was created, and why each choice.**
- **Paper, not Vanilla.** Paper is a drop-in Vanilla-compatible server with substantially better tick
performance and a plugin API; unmodified Minecraft clients connect to it exactly as they would to
Vanilla, so nothing is given up on the player side. Vanilla would only be the right pick if the
operator wanted strict parity with Mojang's own jar or a mod loader (Fabric/Forge/NeoForge are also
in Crafty's bucket if that changes). Purpur and Folia were available and not chosen: Purpur adds
gameplay-changing configuration nobody asked for, and Folia's threaded-regions model targets very
large player counts and is the wrong shape for a home server.
- **Version 26.2.** Mojang's `version_manifest_v2.json` reports `latest.release = 26.2`, released
2026-06-16 — about three months mature, so not a fresh drop. The current snapshot is `26.3-rc-2`,
and Crafty's bucket also lists `26.2-rc-2`; both were deliberately avoided, as the brief asked for a
current stable release and not a snapshot. Crafty verifies the jar against a published SHA256.
- **A 2 GiB heap (`-Xms2000M -Xmx2000M`), not larger.** This was measured, not guessed. The container's
memory limit is 4Gi. With the server idle-but-running, the cgroup reported `memory.current` of
2.86Gi against a 4Gi `memory.max` — that is the 2 GiB committed heap, plus roughly half a gigabyte of
JVM non-heap (metaspace, code cache, GC structures, thread stacks, memory-mapped region files), plus
the Crafty panel itself. A 3 GiB heap would have landed at or above the 4Gi cap and invited an
OOMKill. Minimum and maximum heap are set equal, which is the standard Paper recommendation to avoid
heap-resize pauses. Node `k3s-prod-3` has about 31 GiB allocatable and was only 38 percent used, so
the node was never the constraint — the container limit was.
**The one repo change.** `platform/crafty/deployment.yaml`: the crafty container's memory *request*
went 1Gi to 3Gi. The limit is untouched at 4Gi. Before a server existed, a 1Gi request was honest;
now that a 2 GiB heap is permanently committed, a 1Gi request understates real usage roughly threefold,
which both misleads the scheduler and makes this Burstable pod an early eviction candidate under node
memory pressure. Nothing else was altered — not the L4 Services, not the NetworkPolicy, not the PVC.
The existing egress policy already allowed `world`, so the jar download needed no new rule.
**A trap worth recording: the create API silently ignores `autostart`.** The server-creation schema
accepts `autostart` and `crashdetection` and validates them happily, but the values never reach the
database — the stored columns are `auto_start` and `crash_detection`, and they came back `false` after
a creation that had asked for `true`. They must be set with a separate `PATCH /api/v2/servers/<uuid>`.
Had I trusted the create call, I would have reported autostart working when it was not, and the server
would have stayed dead after the next pod restart. Both are now `true`, verified by reading them back.
**THIS SERVER IS APP STATE. IT IS NOT IN GIT, AND A REBUILD FROM GIT ALONE WILL NOT RECREATE IT.**
Same shape as the arr download-client rows. What git holds is only the *capacity* to run Minecraft
servers: the Deployment, the PVC, the L4 Services and the policies. The server itself lives in two
places, both on the `crafty-data` Longhorn PVC and neither reconstructible by Flux:
1. **Crafty's SQLite database** (`/crafty/app/config/db/`, the `config` subPath) — the row defining
server `13c7b85d-00bb-4a58-8c86-2433ca47891c` ("homelab"): its name, port, jar, launch command,
heap sizes, autostart and crash-detection flags.
2. **The server directory** (`/crafty/servers/13c7b85d-00bb-4a58-8c86-2433ca47891c/`, the `servers`
subPath) — `paper.jar`, `eula.txt`, `server.properties`, and **the generated world**, which is the
genuinely irreplaceable part. A jar can be re-downloaded; a world cannot be regenerated.
If that PVC is lost and restored from nothing, `flux reconcile` brings back a healthy, *empty* Crafty
with no servers and a refused connection on 25565 — precisely the state this entry is closing out.
Recreating it by hand means: create the server in Crafty (Paper, 26.2, port 25565, 2 GiB heap), accept
the EULA again, PATCH `auto_start` on — and accept that the world starts over. **The world is
therefore backup-relevant in a way the manifests are not**, which makes the still-open no-backups gap
(Proxmox migration entry) materially worse than it was yesterday: there is now player-authored state on
this cluster. Crafty has its own scheduled-backup feature writing to the `backups` subPath, but that is
same-PVC and so protects against an in-app mistake, not against loss of the volume. Configuring it, and
getting the volume itself into a real backup, are both still open and both need the operator.
**Proofs, run in the order the brief asked for.** Every one of these is real output, not inference.
(a) Crafty reports started running=True ver='Paper 26.2' players=0/20
server log: 'Done (12.737s)! For help, type "help"'
(b) in-cluster, cross-node from a pod on k3s-prod-1 while crafty ran on k3s-prod-3,
python3 sockets (no curl in these images):
10.0.1.95:25565 pod IP SLP OK
10.43.5.109:25565 ClusterIP SLP OK
192.168.2.22:25565 the VIP SLP OK
(c) from the LAN, VIP workstation host, bash /dev/tcp to 192.168.2.22:25565 -> connect OK
(d) Server List Ping full handshake -> status -> ping/pong, from the workstation:
MOTD 'A Minecraft Server' Version 'Paper 26.2'
Protocol 776 Players 0/20
round trip 12.3 ms enforcesSecureChat True
Proof (d) is the one that matters: it is the same exchange a Minecraft client performs to draw the
server in its list, so it demonstrates the game protocol end to end rather than merely an open port.
**Restart persistence — tested, and it moved nodes.** Deleting the pod rescheduled Crafty from
`k3s-prod-3` onto `k3s-prod-1`. It came back `2/2 Running`, and with **no manual start issued** the
server auto-started about 30 seconds later: same UUID, same world directory, `eula=true` intact,
`Paper 26.2` answering. The LAN Server List Ping was then re-run against the VIP and still succeeded
with the pod on a different node — which independently re-confirms the `externalTrafficPolicy: Cluster`
fix from `adda6dd`, since under the old `Local` policy the VIP would have blackholed exactly here.
**Left alone and verified unbroken:** the oauth2-proxy sidecar still returns 302 to
`keycloak.home.keithjackson.dev/realms/homelab/protocol/openid-connect/auth` for an unauthenticated
request, so the Keycloak gate on the web UI is intact. Note that `crafty.home.keithjackson.dev` does
not resolve from the workstation shell — that is the pre-existing split-horizon DNS gap described in
the entry above, not a regression from this work.
**Still open for the operator:** review the EULA acceptance; decide whether the defaults are wanted
(MOTD is still "A Minecraft Server", 20 player slots, no whitelist, online-mode on, and **the server is
open to anyone who can reach the VIP** — a whitelist or `loadBalancerSourceRanges` is a judgement call I
did not make alone); and get the world into a backup.