jerry@homelab:~/projects$ cat minecraft.md
The Minecraft servers
The kids play on three Minecraft servers at home: a main creative server, Survival and One Block. Each one is its own Docker stack with its own world and player data, running Paper with Geyser and Floodgate so Java and Bedrock players can play together. All three are LAN-only: no port is forwarded on the router. A plan to open the main server to friends was never carried out.
Day to day, they're run by mc-tool: it puts them to sleep when nobody plays, lets the kids start them again, handles the whitelists, and delivers the holiday gifts. This page is about the servers themselves.
Bedrock's new network protocol
Bedrock 26.60 dropped RakNet, the protocol Bedrock clients had always used. Clients now only connect via NetherNet: first an HTTP request over TCP to the server's Bedrock port (signalling), then the game itself over WebRTC on UDP. I switched all three servers a few days before that update came out and tested from the tablet. Getting it right took more than flipping a switch:
- Both TCP and UDP. Each server now publishes its Bedrock port for both protocols, not just UDP.
- The server has to tell clients where to go. During signalling, Geyser tells the client which address and port to play on. Inside its container it only knows its container address and the internal port, which is the same for all three servers. Without an explicit advertise address, Survival and One Block players would have been sent to the wrong server, or to my public IP.
- Settings through system properties. Geyser manages its own config file, and that file still says
raknet; system properties in the compose file win. They go inJVM_OPTS, notJVM_DD_OPTS, because the image splitsJVM_DD_OPTSon:, which breaks anaddress:portvalue. - A trust prompt. The first join per server per device asks to trust the server, because signalling is plain HTTP. Expected; tap allow.
- Not over Tailscale. Only the LAN address is advertised, so a Bedrock client on Tailscale would be sent to an address it can't reach. Java over Tailscale still works.
- Geyser updates itself on every start. An older build only accepts Bedrock clients up to the version it knows, so after a Bedrock update the fix is simply restarting the servers.
The rebuild NetherNet forced
Geyser's NetherNet support needs Netty 4.2, and Geyser uses the server's own Netty. One Block ran on an old Paper version, pinned there because its one-block plugin and its world plugins only worked on that version, and that Paper still shipped Netty 4.1. It failed with NoClassDefFoundError: io/netty/channel/MultiThreadIoEventLoopGroup: Bedrock players would have been locked out of One Block after the update.
Rather than run a separate Geyser next to it, I rebuilt One Block on the current version with BentoBox + AOneBlock. AOneBlock creates its own void world, so the extra world plugins went too, and the household now plays on one shared island. The kids' old progress wasn't migrated on purpose: they weren't far, and it had stopped being fun. Two lessons from the rebuild:
- The image's Modrinth installer only writes to
plugins/. AOneBlock is a BentoBox addon, which lives in a subfolder of BentoBox. Installed the normal way it failed to load, so it's placed and pinned by hand. - A world created later gets no map. BlueMap, the live web map, only creates map configs on its very first start, for the worlds that exist then. AOneBlock's island world came later, so its map config was added by hand.
Permissions that are broader than they look
For the kids to have fun, they need /gamemode and /tp. The obvious way is granting the vanilla permission nodes, and two of those are much more dangerous than they look:
minecraft.command.gamemodelets you change anyone's gamemode to anything, not just your own. One kid in creative mode can fly into a sibling's build and break it instantly.minecraft.command.teleportalso covers teleporting other players, with no confirmation, so a friend can be moved somewhere dangerous by accident.
Vanilla has no "only yourself" version of either. EssentialsX, which was already installed, has finer permissions: switching your own gamemode between creative and survival, and /tpa, which sends a request the other player has to accept. The part that actually matters is removing the broad vanilla nodes. Adding a narrower permission next to a broader one that already grants the same thing doesn't restrict anything.
Anti-grief: Prism
For logging and rollback on the main server I chose Prism, after looking at the two obvious alternatives:
- CoreProtect is behind a paywall for Minecraft versions above 1.21.11, and our servers are well past that. Development builds exist, but only through a self-compile or Patreon, which doesn't fit the automated plugin install the servers use.
- StellarProtect is newer, with a responsive developer, but at least one credible review reported a restore filling an area with lava that was never there. A rollback tool that can make the damage worse is exactly the wrong risk.
- Prism has a long track record, a recent rewrite that matched our version exactly, and several "switched from CoreProtect, rollbacks are more reliable" reviews.
Other access control is simpler: online mode on every server (every Java player is checked against their real Microsoft account), an enforced whitelist on the main server and One Block, and Bedrock players whitelisted by their Floodgate UUID.
Backups for servers that sleep
Each server has a backup sidecar that archives the world every six hours. Then mc-tool started putting servers to sleep, and the sidecars didn't like it. Docker's DNS stops resolving a stopped container's name, so the sidecar failed its RCON connection five times, exited, and was restarted by Docker about every three minutes.
The fix was three settings: retry RCON forever instead of exiting, check once a minute, and pause backups while nobody's on. A sleeping server's sidecar now just waits, and since the world doesn't change while the server is stopped, nothing from the last session is missed. After every backup a small script lists the archive's contents with tar -tzf and logs PASS or FAIL, so a corrupted archive shows up in the log. That proves the archive is readable, not that a restore works, so the worlds are also in the nightly restic backups, whose restores have been tested. At night the servers are almost always asleep, so restic gets a clean copy.
One side effect to remember: a deploy wakes a server. Pushing a change to a Minecraft stack runs docker compose up -d, which also starts a sleeping container. mc-tool puts it back to sleep after 15 minutes, but the restart kicks out anyone playing, so I don't deploy while the kids are on.
What's next
A fourth server, only for family event nights: Build Battle, spleef, hide-and-seek and a present hunt on pakjesavond. It's pinned to the version Build Battle supports, with a hand-built lobby and arenas, and mc-tool decides when the kids can start it. See mc-tool for how that fits in.
jerry@homelab:~/projects$ █
cd ..