Minecraft Java Server on Linux — Paper, systemd, and Memory Tuning
Run a Paper Minecraft server on Linux the clean way: own user, Java, systemd unit, Aikar's flags, sane RAM sizing, RCON console, firewall and safe backups.
No panel, no screen -S mc in root's home directory, no 16 GB heap "because more is better". This is how I set up a plain Paper server on a Linux box: own user, systemd, flags that make sense, and a console you can actually reach.
TL;DR:
sudo apt install temurin-21-jre # Adoptium repo, see below
sudo adduser --system --group --home /opt/minecraft minecraft
cd /opt/minecraft
sudo -u minecraft curl -o paper.jar "<paper download url>"
sudo -u minecraft java -Xms4G -Xmx4G -jar paper.jar --nogui # generates files, stops at EULA
echo "eula=true" | sudo -u minecraft tee eula.txt
# drop in the systemd unit below, then:
sudo systemctl enable --now minecraft
sudo ufw allow 25565/tcp
Why Paper and not vanilla or Spigot is its own post: Vanilla vs Spigot vs Paper. Short version: Paper runs Bukkit/Spigot plugins, is faster, and is what every proxy setup on this blog assumes as the backend.
Java
Minecraft 1.20.5 and newer need Java 21. Older Java fails with an UnsupportedClassVersionError the moment you start the jar. I use Eclipse Temurin from the Adoptium repo because the distro's default JDK is either older or moves on its own schedule:
sudo apt install -y wget apt-transport-https gpg
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public \
| sudo gpg --dearmor -o /etc/apt/keyrings/adoptium.gpg
echo "deb [signed-by=/etc/apt/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release; echo $VERSION_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt update && sudo apt install -y temurin-21-jre
java -version
Ubuntu 24.04 and Debian 13 also have openjdk-21-jre-headless in the main repos, which works just as well. Whatever you pick, check what your Paper version wants on its download page — Mojang raises the Java floor every few major versions, and the next bump will come.
Gotcha: several Javas installed
If java -version shows 17 even though you installed 21, the alternatives system still points at the old one:
sudo update-alternatives --config java
Or skip the question entirely and put the full path (/usr/lib/jvm/temurin-21-jre-amd64/bin/java) into the systemd unit. That's what I do — the server keeps working when someone installs a different JDK for something else.
User and directory
sudo adduser --system --group --home /opt/minecraft minecraft
System user, no login shell, no password. The server can only write in its own directory. If a plugin ever does something stupid, it does it as minecraft, not root. If the box itself isn't set up yet, run through my Ubuntu initial setup checklist first.
Get Paper
PaperMC moved their download API (now called Fill, v3) and the old v2 endpoints are being phased out. The safe way: open papermc.io/downloads/paper, copy the link for the version you want, and curl it. Scripted:
VER=1.21.8
URL=$(curl -s -A "my-server-setup (admin@example.com)" \
"https://fill.papermc.io/v3/projects/paper/versions/$VER/builds/latest" \
| jq -r '.downloads."server:default".url')
sudo -u minecraft curl -A "my-server-setup (admin@example.com)" -o /opt/minecraft/paper.jar "$URL"
The API wants a User-Agent that identifies you — the default curl one can get rejected. Pin the version. "Latest" sounds convenient until a major release lands and half your plugins don't load.
First start generates the files, then stops at the EULA:
cd /opt/minecraft
sudo -u minecraft java -Xms2G -Xmx2G -jar paper.jar --nogui
echo "eula=true" | sudo -u minecraft tee eula.txt
server.properties — what I actually change
server-port=25565
motd=My Server
max-players=20
view-distance=8
simulation-distance=6
online-mode=true
white-list=true
enforce-whitelist=true
enable-rcon=true
rcon.port=25575
rcon.password=CHANGE-ME-LONG-RANDOM
- view-distance / simulation-distance — the biggest performance knobs you have. View distance is what players see, simulation distance is what actually ticks (mobs, crops, redstone). Lowering simulation distance does more for TPS than any JVM flag.
- white-list — on for anything not meant to be public. Bots scan port 25565 constantly.
If this server will sit behind a proxy, a few of these change (
online-mode=false, bind to127.0.0.1) — that's covered in the Velocity setup.
Memory: how much, and why less is often more
Rules I stick to:
- Xms = Xmx. Same value. The JVM doesn't have to grow the heap under load, and with
AlwaysPreTouchthe memory is committed at startup so you know immediately whether it fits. - Leave RAM for the OS. The JVM uses more than the heap — metaspace, thread stacks, direct buffers, plus the page cache your world files live in. On an 8 GB box I'd give the server 5–6 GB, not 8.
- Don't oversize. A bigger heap doesn't make an unoptimized server faster. It makes GC pauses rarer but longer. If a small server "needs" 16 GB, there's a chunk loader, an entity farm or a leaking plugin somewhere — fix that.
- Pre-generate the world. Chunk generation is the most expensive thing a server does. Chunky (a plugin) pre-generates a radius before players arrive, and it makes a bigger difference than any heap tuning.
Aikar's flags
These are the G1GC flags the Paper docs still recommend. For heaps up to 12 GB:
-Xms6G -Xmx6G -XX:+AlwaysPreTouch -XX:+DisableExplicitGC -XX:+ParallelRefProcEnabled
-XX:+PerfDisableSharedMem -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC
-XX:G1HeapRegionSize=8M -XX:G1HeapWastePercent=5 -XX:G1MaxNewSizePercent=40
-XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1NewSizePercent=30
-XX:G1RSetUpdatingPauseTimePercent=5 -XX:G1ReservePercent=20 -XX:InitiatingHeapOccupancyPercent=15
-XX:MaxGCPauseMillis=200 -XX:MaxTenuringThreshold=1 -XX:SurvivorRatio=32
-Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true
Above 12 GB, change five values:
-XX:G1NewSizePercent=40 -XX:G1MaxNewSizePercent=50 -XX:G1HeapRegionSize=16M
-XX:G1ReservePercent=15 -XX:InitiatingHeapOccupancyPercent=20
Don't mix and match with flags from random forum posts. These are tuned as a set — the high young-gen percentage only makes sense together with the low tenuring threshold. The same set works for modded servers too.
systemd unit
/etc/systemd/system/minecraft.service:
[Unit]
Description=Minecraft Paper Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=minecraft
Group=minecraft
WorkingDirectory=/opt/minecraft
ExecStart=/usr/lib/jvm/temurin-21-jre-amd64/bin/java -Xms6G -Xmx6G \
-XX:+AlwaysPreTouch -XX:+DisableExplicitGC -XX:+ParallelRefProcEnabled \
-XX:+PerfDisableSharedMem -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC \
-XX:G1HeapRegionSize=8M -XX:G1HeapWastePercent=5 -XX:G1MaxNewSizePercent=40 \
-XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1NewSizePercent=30 \
-XX:G1RSetUpdatingPauseTimePercent=5 -XX:G1ReservePercent=20 \
-XX:InitiatingHeapOccupancyPercent=15 -XX:MaxGCPauseMillis=200 \
-XX:MaxTenuringThreshold=1 -XX:SurvivorRatio=32 \
-Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true \
-jar paper.jar --nogui
Restart=on-failure
RestartSec=15
TimeoutStopSec=120
SuccessExitStatus=0 143
MemoryMax=7G
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now minecraft
sudo journalctl -u minecraft -f
Wait for Done (x.xxxs)! For help, type "help".
The lines that matter:
TimeoutStopSec=120— onsystemctl stop, systemd sends SIGTERM and the server saves all worlds before exiting. A big world needs more than the default 90 seconds; if systemd SIGKILLs mid-save, you get corrupted chunks.SuccessExitStatus=0 143— Java exits with 143 on SIGTERM. Without this, every clean stop shows up as "failed".MemoryMax=7G— heap plus overhead. A hard ceiling so a leak kills the server, not the whole box.
More on the options: systemd service unit files.
A console without screen
systemd gives the server no stdin, so you can't type commands into it. RCON fixes that. It's already enabled in server.properties above. Use mcrcon (grab a release from its GitHub) or any RCON client:
mcrcon -H 127.0.0.1 -P 25575 -p 'CHANGE-ME-LONG-RANDOM' -t
-t gives you an interactive terminal. Single commands work too: mcrcon ... "say restart in 5 minutes".
Gotcha: RCON is plaintext
The RCON protocol is unencrypted and the password goes over the wire as-is. Never open 25575 to the internet. Only talk to it from localhost or over a VPN:
sudo ufw allow 25565/tcp
sudo ufw deny 25575/tcp
Minecraft Java is TCP-only — no UDP rule needed unless you run a Bedrock bridge or voice-chat plugin. More rules: UFW firewall rules.
Backups that don't corrupt
Copying the world folder while the server writes to it gives you a backup that might not load. Pause saving first:
#!/bin/bash
RCON="mcrcon -H 127.0.0.1 -P 25575 -p CHANGE-ME-LONG-RANDOM"
$RCON "save-off" "save-all flush"
sleep 5
rsync -a --delete /opt/minecraft/ /backup/minecraft/$(date +%F)/
$RCON "save-on"
save-all flush blocks until everything is on disk. Put it on a timer, and make sure save-on runs even if rsync fails — otherwise the server silently stops saving. Full pattern: rsync backups on Linux.
Gotcha: TPS is bad and RAM isn't the problem
Before touching flags or memory, run the profiler that's already there:
/spark profiler start
Spark ships with Paper these days. Let it run a few minutes during lag, /spark profiler stop, open the link. It shows you exactly which entity, plugin or chunk is eating the tick. Nine times out of ten it's an entity pile-up or a plugin, not the JVM.
Next steps
One server is fine for friends. Once you want a lobby plus survival plus creative, put a proxy in front: Velocity from scratch. If it should stay friends-only, the whitelist plus a firewall does most of the work.