Side Projects
I put a multiplayer Minecraft server on a Pi — then taught it to build real places
A PaperMC server, a custom dashboard, and an OpenStreetMap world generator — all sharing one Raspberry Pi 5 that was already running everything else.
My Raspberry Pi 5 was already doing a lot. It serves this blog, runs Plex, keeps Home Assistant ticking, and answers for a handful of little web apps. So when I wanted a Minecraft world of my own — somewhere a small group could actually play together — the honest question wasn't "where do I host this?" It was "can the box I already own quietly take on one more thing?"
It could. And then that one more thing turned into three.
A small, private world
I didn't want a big public server with strangers and grief and moderation. I wanted a small, private place — invite-only, somewhere a few people could log in and build something together.
So that's exactly what it is. The whitelist is on by default, it's capped at ten players, and the message of the day is nothing more than a plain greeting. It's survival mode, normal difficulty, nothing fancy. The whole point is that it's private and it's quiet.
![]()
Squeezing it onto a Pi that's already busy
Here's the constraint that shaped everything: the Pi has 8 GB of RAM, and Minecraft was the last guest at a table that was already pretty full.
The fix was to be disciplined about it. The server is PaperMC — a performance-tuned fork that's far lighter than vanilla — running in Docker via the excellent itzg/minecraft-server image. I capped its memory at 2 GB and turned on the Aikar JVM flags, which tune garbage collection so a memory-limited server doesn't stutter.
Then I dialed the world down to fit an ARM chip: view distance of 8, simulation distance of 6, ten players max. None of that is noticeable when you're in the game. All of it matters when you're sharing a CPU with Plex transcodes and a database.
The lesson I keep relearning with this Pi: constraints aren't the enemy. They just force you to pick numbers on purpose instead of accepting defaults.
![]()
Crossplay, because not everyone's on a PC
The catch with any shared server is that nobody owns the same hardware. One player's on a laptop, someone else only has a phone, and the console crowd plays Bedrock — a different edition that normally can't talk to the Java server I was running.
Geyser and Floodgate fix that. Geyser translates Bedrock players into something the Java server understands; Floodgate lets them in without needing a separate Java account. Both auto-install as plugins when the container starts, so there's nothing to maintain.
The result is the part I'm quietly proud of: someone on a tablet and someone on a desktop can stand in the same world, building the same house, and neither of them had to care which "edition" they own.
![]()
I got tired of SSH-ing in, so I built a dashboard
For the first little while, every request was a terminal session. "Can you whitelist someone?" meant SSH into the Pi, exec into the container, type the RCON command. "It's being weird" meant logging in to restart it. That gets old fast — especially when you're the only one who can do it.
So I wrote a small admin dashboard in Flask. It lives behind an authenticated tunnel — a private address I don't hand out — and it turns all the ops chores into buttons: toggle the whitelist, add or remove players, op someone, restart the server, see who's online and kick if needed. There's even a live feed of in-game chat, scraped from the server log, so I can glance at what's happening without opening the game.
It also has real user accounts with roles, so I can hand out limited access without giving anyone the keys to the Pi. The whole thing talks to the server over RCON and to Docker for restarts. It's maybe the most "this is actually useful" piece of the project — the server is the toy, but the dashboard is what makes it livable to run.
![]()
The wild part: playing in a real place
This is the feature that started as a "wouldn't it be cool if" and somehow shipped.
The dashboard has a world generator, and the part I'm proudest of is that it's completely self-serve. Anyone with access opens a map, draws a box around a real location — a neighborhood, a stretch of a city, anywhere — gives it a name, and hits generate. Behind the scenes it pulls the actual building footprints, roads, and terrain from OpenStreetMap and renders that place as a Minecraft world. It's built on a tool called arnis, which I wrapped so the whole thing happens in the browser instead of on a command line.
![]()
Generation runs in the background and streams its progress to the page. When it's done, the new world is saved — and then comes the part that genuinely delighted me: applying it is a single click. No files, no terminal, no waiting on me to do it. The dashboard stops the server, backs up the current world automatically, drops the new one into place, and starts everything back up — about thirty seconds, no lost saves. Every world anyone generates is kept, so you can swap between them whenever you like.
So anyone can spin up a blocky version of somewhere they actually know and be standing in it a minute later. That never stops being a little magical.
![]()
The part that should scare you
I want to be honest about the trade-off at the center of this, because it's a real one.
For the dashboard to restart the server, it needs to talk to Docker — and the way it does that is by mounting the Docker socket. That's effectively root access to the host. If someone got into the dashboard, they wouldn't just mess with Minecraft; they could own the whole Pi.
The only thing standing in front of that is a password and good hygiene. So the dashboard binds strictly to localhost, it's only reachable through an authenticated Cloudflare tunnel — never raw on the public internet — and the password is long and not reused. That's enough for a small server on a home box. But I don't pretend it's free of risk, and I wouldn't bolt this onto something that mattered more without rethinking that boundary. Convenience and blast radius are always trading against each other; this is just where I drew the line, eyes open.
![]()
Treating a toy like infrastructure
The last habit I brought over from the rest of this Pi: even a game server deserves to be reproducible.
The code and config — the compose file, the dashboard, the tuning — all live in git. The heavy, private stuff — the world saves, the player accounts, the generated-world archives — travels separately in a tarball, because it's big and it's nobody's business but the people who play there. Moving the entire setup to a new machine is a clone, a restore, and one docker compose up. There's even a playit.gg tunnel option for the day it lives behind a connection that won't forward ports.
None of that is necessary for a Minecraft server. But it's the difference between a thing that runs and a thing I can actually rebuild when the SD card eventually dies — and on a Pi, the SD card always eventually dies.
That's the whole story, really. I wanted a place where a few people could play. The box I already owned had room for it. And somewhere between the whitelist and the world generator, a weekend project turned into one of my favorite things running on this little machine.
![]()
Filed under