To run a modern Java Edition MC server on a Raspberry Pi without chunk-generation lag, you must use the Raspberry Pi 5 (8GB variant) paired with an NVMe SSD via the PCIe HAT. MicroSD cards will corrupt within weeks under Minecraft's constant chunk-write I/O, and the older Pi 4 lacks the single-core burst speed required to keep the server's main thread under the 50ms tick threshold.
This guide walks through the exact hardware bill of materials, wiring a physical GPIO status and shutdown circuit, deploying a Python-based hardware monitor, and debugging the specific Java crash loops that plague embedded server builds.
The Hardware Verdict: Sizing the Board and Storage
Minecraft is notoriously single-thread heavy. The main thread handles world ticking, entity AI, and chunk generation. If that thread takes longer than 50 milliseconds to process a tick, players experience rubber-banding and lag. Here is the decision path for selecting your host board:
| Hardware Option | Single-Core PassMark (Approx) | Max Stable Players (Vanilla/Paper) | Verdict |
|---|---|---|---|
| Raspberry Pi 4 (8GB) | ~750 | 3-5 | Reject: Fails at 1.20+ chunk gen. Bottlenecks on USB 3.0 storage bus. |
| Raspberry Pi 5 (8GB) | ~1150 | 8-12 | DEFAULT PICK: PCIe 2.0 lane allows true NVMe speeds. ARM Cortex-A76 handles PaperMC optimizations well. |
| Intel N100 Mini PC | ~1600 | 15-20+ | Upgrade Pick: Choose this only if you need >12 players or heavy modpacks. Draws 15W+ vs the Pi's 8W. |
Exact Parts List (Pi 5 Build)
- Board: Raspberry Pi 5 (8GB RAM) - Do not buy the 4GB variant; the JVM and Linux OS will fight for memory.
- Power: Official Raspberry Pi 27W USB-C PD Power Supply ($12). Third-party chargers often drop voltage under transient CPU loads, triggering throttling.
- Cooling: Official Active Cooler ($5). Passive heatsinks will thermal throttle at 80°C during world generation.
- Storage HAT: Geekworm X1001 NVMe PCIe HAT ($15).
- Storage Drive: 256GB M.2 NVMe SSD (e.g., Western Digital SN570, $25). Do not use DRAM-less QLC drives; they stall on sustained random writes.
Wiring the GPIO Status and Shutdown Circuit
Running a headless server means you need physical feedback. We will wire a status LED to indicate server health and a physical button to trigger a graceful `stop` command, preventing world corruption from hard power cuts.
Pin Mapping Table
| Component | Pi 5 GPIO Pin (BCM) | Physical Pin # | Wiring Notes |
|---|---|---|---|
| Status LED (Anode) | GPIO 17 | 11 | Use a 330Ω current-limiting resistor in series. |
| Status LED (Cathode) | GND | 9 | Connect to common ground rail. |
| Shutdown Button (Leg 1) | GPIO 27 | 13 | Internal pull-up enabled in software. |
| Shutdown Button (Leg 2) | GND | 14 | Pressing bridges GPIO 27 to GND. |
Server Deployment and the Python Hardware Monitor
This Python script targets the Raspberry Pi 5 8GB running Raspberry Pi OS (64-bit, Bookworm). It launches the PaperMC server, monitors CPU temperature, blinks the LED if the system is thermal throttling, and catches the physical button press to execute a graceful shutdown.
Prerequisites: Run sudo apt install python3-gpiozero python3-psutil and download the paper.jar into the same directory.
import time
import subprocess
import os
import signal
import psutil
from gpiozero import LED, Button
from gpiozero.exc import GPIOZeroError
# --- PIN DEFINITIONS ---
STATUS_LED = LED(17)
SHUTDOWN_BTN = Button(27, pull_up=True, bounce_time=0.2)
# --- SERVER CONFIGURATION ---
SERVER_DIR = "/home/pi/minecraft"
JAR_NAME = "paper.jar"
# Aikar's optimized JVM flags for 8GB Pi (allocating 5G to heap, leaving 3G for OS/Disk Cache)
JVM_FLAGS = [
"-Xms5G", "-Xmx5G", "-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200",
"-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC",
"-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=40",
"-XX:G1HeapRegionSize=8M", "-XX:G1ReservePercent=20",
"-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=4",
"-XX:InitiatingHeapOccupancyPercent=15", "-XX:G1MixedGCLiveThresholdPercent=90",
"-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32",
"-XX:+PerfDisableSharedMem", "-XX:MaxTenuringThreshold=1"
]
class MinecraftServerManager:
def __init__(self):
self.process = None
self.running = True
def start_server(self):
cmd = ["java"] + JVM_FLAGS + ["-jar", JAR_NAME, "--nogui"]
print("[INFO] Starting MC Server...")
STATUS_LED.on()
try:
self.process = subprocess.Popen(
cmd,
cwd=SERVER_DIR,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
except FileNotFoundError:
print("[ERROR] Java not found or jar missing. Check paths.")
self.running = False
def send_command(self, cmd_str):
if self.process and self.process.poll() is None:
self.process.stdin.write(cmd_str + "\n")
self.process.stdin.flush()
def monitor_hardware(self):
"""Checks CPU temp and blinks LED if thermal throttling is imminent."""
try:
temps = psutil.sensors_temperatures()
if 'cpu_thermal' in temps:
current_temp = temps['cpu_thermal'][0].current
if current_temp > 75.0: # Pi 5 throttle threshold is 80C
STATUS_LED.blink(on_time=0.2, off_time=0.2, background=True)
print(f"[WARN] High Temp: {current_temp}C")
else:
STATUS_LED.on()
except Exception as e:
print(f"[WARN] Sensor read failed: {e}")
def graceful_shutdown(self):
print("\n[INFO] Physical button pressed. Sending 'stop' to server...")
STATUS_LED.blink(on_time=0.5, off_time=0.5)
self.send_command("stop")
# Wait up to 30 seconds for Java to flush chunks and exit
for _ in range(30):
if self.process.poll() is not None:
break
time.sleep(1)
if self.process.poll() is None:
print("[WARN] Server hung. Force killing Java process.")
self.process.kill()
print("[INFO] Server stopped. Halting Pi.")
os.system("sudo shutdown -h now")
def run_loop(self):
SHUTDOWN_BTN.when_pressed = self.graceful_shutdown
self.start_server()
while self.running:
if self.process and self.process.poll() is not None:
print("[ERROR] Server process died unexpectedly.")
STATUS_LED.off()
self.running = False
break
self.monitor_hardware()
time.sleep(5)
if __name__ == "__main__":
manager = MinecraftServerManager()
try:
manager.run_loop()
except KeyboardInterrupt:
manager.graceful_shutdown()
except GPIOZeroError as e:
print(f"[FATAL] GPIO Hardware Fault: {e}")
Save this as server_manager.py and run it via a systemd service so it starts on boot and restarts if the Python wrapper crashes.
Debugging: Fixing the Crash Loops
When embedding a Java workload on an ARM SBC, you will hit specific failure modes. Here is the exact error strings and how to fix them.
Error 1: java.lang.OutOfMemoryError: Java heap space
Ranked Causes & Fixes:
- Cause: The
-Xmxflag is set too high, starving the Linux OS of RAM for disk caching, causing the OOM killer to terminate Java.
Fix: On an 8GB Pi, never exceed-Xmx5G. Leave 3GB for the OS and NVMe page cache. - Cause: A poorly coded plugin is leaking memory.
Fix: Install thesparkprofiler plugin and run/spark profiler --timeout 300to identify the offending class. - Cause: View distance is set too high in
paper-world.yml.
Fix: Capview-distanceat 8 andsimulation-distanceat 6.
Error 2: [Server thread/WARN]: Can't keep up! Is the server overloaded? Running 4500ms behind
Ranked Causes & Fixes:
- Cause: Storage I/O bottleneck. The server is waiting on the disk to write chunk data.
Fix: Verify you are actually booted from the NVMe drive. Runlsblk. If/is mounted onmmcblk0(the SD card), your bootloader EEPROM isn't configured to prioritize PCIe. Runsudo rpi-eeprom-config --editand setBOOT_ORDER=0xf416. - Cause: CPU Thermal Throttling.
Fix: Checkvcgencmd get_throttled. If it returns anything other than0x0, your Active Cooler is either unseated or the 27W PSU is dropping voltage under load.
First Three Things to Check When It Fails
If the server goes down and the Python script doesn't catch it, run these three diagnostic commands immediately via SSH:
- Check for Undervoltage Throttling:
dmesg | grep -i undervoltage
If you see "Under-voltage detected!", your power supply or USB-C cable is inadequate. The Pi 5 requires a strict 5V/5A PD negotiation. - Check NVMe Link State:
dmesg | grep -i pcie
Look for "link down" or AER (Advanced Error Reporting) correctable errors. If the NVMe drops offline, you may need to addpcie_aspm=offto your/boot/firmware/cmdline.txtto disable aggressive power-saving on the PCIe bus. - Check Java Core Dumps:
journalctl -u minecraft-server.service -n 50
Look for SIGSEGV. If Java is segfaulting on ARM64, ensure you are using the Eclipse Temurin JDK 21 ARM64 build, not the default Debian OpenJDK, which occasionally has JIT compiler bugs on ARM Cortex-A76.
Extending or Simplifying Your Build
Depending on your player count and maintenance tolerance, you should adjust the architecture.
How to Simplify (The "Set and Forget" Route)
If you don't want to manage JVM flags, Python scripts, or GPIO wiring, abandon the bare-metal approach. Flash Ubuntu Server 24.04 LTS (ARM64) onto the NVMe drive and deploy PaperMC via Docker using the itzg/minecraft-server image. You lose the physical GPIO button integration, but the Docker watchdog handles crash restarts automatically, and environment variables handle all JVM tuning.
How to Extend (The "Networked Maker" Route)
To push this build further into embedded territory:
- Add an I2C OLED Display: Wire an SSD1306 128x64 OLED to the I2C pins (GPIO 2/3). Modify the Python script to poll the PaperMC RCON port and display live player counts and TPS (Ticks Per Second) directly on the Pi's chassis.
- Implement Tailscale: Instead of port-forwarding 25565 on your home router (a massive security risk), install Tailscale on the Pi. This creates a WireGuard mesh network, allowing your friends to connect directly to the Pi's virtual IP without exposing your home network to the open internet.
- Automated Backups: Add a secondary USB flash drive and write a
cronjob that usesrsyncto mirror the/worlddirectory every 6 hours. SD cards and cheap NVMe drives can fail without warning; the Raspberry Pi hardware is robust, but flash storage is always a consumable.






