Posted on

Table of Contents

Originally written December 2021, updated September 2024

Got feedback? email yawnbox at disobey dot net

Introduction

I started using Purpur in order to support vanilla Minecraft plugins and for server performance enhancements. I am also using Adoptium Java for the stability, performance, and security benefits rather than using OpenJDK. I like running Purpur becuase its vanilla minecraft but then allows the use of more advanced plugins. The plugins are all server-side, so the clients get them automatically, unlick modded Minecraft.

Setup and Install Purpur

I'm using Ubuntu Server 22.04 LTS at the time of writing.

Install Java

Install Adoptium's Temurin 22 for Java: https://adoptium.net/installation.html

sudo mkdir -p /etc/apt/keyrings

sudo wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | tee /etc/apt/keyrings/adoptium.asc

sudo echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list

sudo apt update && apt install temurin-22-jdk zip -V

Validate the Java version:

/usr/bin/java --version

The output should look like:

openjdk 22.0.2 2024-07-16
OpenJDK Runtime Environment Temurin-22.0.2+9 (build 22.0.2+9)
OpenJDK 64-Bit Server VM Temurin-22.0.2+9 (build 22.0.2+9, mixed mode, sharing)

Create an isolated Linux user, for security

Create a limited user that will be running the Minecraft server:

sudo adduser --system --home /home/minecraft --group --shell /usr/sbin/nologin minecraft

Create a new folder for the server and enter that folder:

sudo mkdir /home/minecraft/purpur && cd /home/minecraft/purpur

Download Purpur

You can download the latest Purpur by going here in a web browser:

https://purpurmc.org

Copy the link to download it to your server folder (ie: /home/minecraft/purpur):

sudo wget https://api.purpurmc.org/v2/purpur/1.21.1/latest/download --content-disposition

Make the jar file is executable:

sudo chmod +x purpur-1.21.1-2303.jar

Create s systemd service, for security, and ease of use

Note: I would have used a Java Security Manager policy to harden Java, but that's being depreciated in newer versions of Java. Since we want to always use the most recent version possible for security and performance reassons, I use a hardened systemd service to manage Java, Purpur, and Minecraft.

Create a new Minecraft systemd service:

sudo vim /etc/systemd/system/minecraft.service

Copy and paste all of these lines into the minecraft.service file:

[Unit]
Description=Minecraft Server
After=network.target

[Service]
# Run as the 'minecraft' user and group
User=minecraft
Group=minecraft

# Restrict network access to specified IPs
IPAddressAllow=103.232.207.250
IPAddressAllow=2620:18c:0:192::250

# Security Enhancements
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
LockPersonality=true
#MemoryDenyWriteExecute=true
NoNewPrivileges=true
ProtectSystem=full
#ProtectHome=true
ProtectClock=true
PrivateDevices=true
ProtectKernelLogs=true
#PrivateNetwork=true
PrivateTmp=true
PrivateUsers=true
ProcSubset=pid
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=strict
RemoveIPC=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallFilter=@system-service
SystemCallArchitectures=native

# Paths related to the Minecraft server
ReadOnlyPaths=/usr/bin/java
ReadOnlyPaths=/usr/lib/jvm
ReadWritePaths=/home/minecraft/purpur
WorkingDirectory=/home/minecraft/purpur

# JVM flags for performance optimization
ExecStart=/usr/bin/java -Xmx16G -Xms16G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 \
-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=40 \
-XX:G1MaxNewSizePercent=50 -XX:G1HeapRegionSize=16M -XX:G1ReservePercent=15 -XX:G1HeapWastePercent=5 \
-XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=20 -XX:G1MixedGCLiveThresholdPercent=90 \
-XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 \
--add-modules=jdk.incubator.vector -jar /home/minecraft/purpur/purpur-1.21.1-2303.jar --nogui

[Install]
WantedBy=multi-user.target

Important minecraft.service file notes:

IPAddressAllow=103.232.207.250
IPAddressAllow=2620:18c:0:192::250

^ These two lines limit which IPv4 and IPv6 addresses can be used. Be sure to change these to your IPs. If you don't have an IPv6 address, remove the line.

ReadOnlyPaths=/usr/bin/java
ReadOnlyPaths=/usr/lib/jvm

^ These two paths are to make sure Java can be called. The JVM folder is where Temurin is installed. These lines shouldn't need to change.

ReadWritePaths=/home/minecraft/purpur
WorkingDirectory=/home/minecraft/purpur

^ These two paths are the same, as they are where Purpur is downloaded to. If you want to install purpur into /opt instead, then these lines should change.

-Xmx16G -Xms16G

^ This flag must change based on how much static RAM you want to give the Minecraft server.

-jar /home/minecraft/purpur/purpur-1.21.1-2303.jar

^ This jar flag indicates the specified purpur file with its explicit version that must be launched. This needs to be manually updated every time purpur is updated.

For descriptions of all of the java arguments used, see the bottom of this article.

Continue systemd setup

Save the minecraft.service file (via vim):

:wq

Reload systemd:

sudo systemctl daemon-reload

Note: everytime the minecraft.service file is updated, this daemon-reload must be re-run.

Enable the new service:

sudo systemctl enable minecraft.service

Accept the EULA in advance of running the first time (from within /home/minecraft/purpur):

sudo rm -f eula.txt && sudo touch eula.txt && sudo echo 'eula=true' >> eula.txt && sudo cat eula.txt

Presuming you have already enabled the inbound firewall for server security, open up the Minecraft port. I use "limit" instead of "allow" to prevent DoS attacks:

sudo ufw limit 25565/tcp && sudo ufw reload

Start Minecraft for the first time to setup the files:

sudo systemctl start minecraft.service

You can see the service status with:

sudo systemctl status minecraft.service

You can see the logging output with:

sudo journalctl -u minecraft.service -f

Note about the first run

When I ran the start script the first time, I saw "Downloading mojang_1.20.1.jar" for a long time and nothing seemed to be happening. To test, I downloaded it manually to my minecraft server just to see if there was a connection issue, but it seems it's just a very slow download. Less than 100 KB/s yet only a 46 MB file. Could take as long as 20 minutes, so just be patient. Manually downloading the server.jar file does not speed up the script.

After the download completes, the script will start Minecraft for the first time.

Configure Purpur for the first time

Stop the service:

sudo systemctl stop minecraft.service

There are now many other files and folders in the /home/minecraft/purpur directory.

Edit the server.properties file:

sudo vim server.properties

Things I typically change:

motd=your.domain.com
difficulty=hard
max-players=9000
allow-flight=true
view-distance=32
server-ip=1.2.3.4
server-name=your.domain.com
level-type=AMPLIFIED

Then start the server back up!

sudo systemctl start minecraft.service

Have fun!

Plugins

If and when you want to install any plugins, first download the plugin jar files then restart the server service. I'd watch YouTube about the most popular plugins for ideas.

cd /home/minecraft/purpur/plugins/

MineTinker

wget https://github.com/Flo56958/MineTinker/releases/download/v1.9/MineTinker.jar

WorldEdit (https://modrinth.com/plugin/worldedit/versions?l=bukkit)

wget https://cdn.modrinth.com/data/1u6JkXh5/versions/yAujLUIK/worldedit-bukkit-7.3.6.jar

WorldGuard (not currently accessible)

wget https://dev.bukkit.org/projects/worldguard/files/latest --content-disposition

Restart the systemd service:

sudo systemctl restart minecraft.service

Maintenance

To backup a world manually:

sudo zip -r /home/minecraft/purpur/world.zip /home/minecraft/purpur/world/

To delete a world and start over (while the server is stopped):

sudo rm -r /home/minecraft/purpur/world/

Be sure to keep Adoptium's Java up to date, and of course all other system packages:

sudo apt update && sudo apt dist-upgrade -V && sudo apt autoremove -y && sudo apt autoclean

Java arguments explained

RAM management flags

-Xmx16G:
This sets the maximum heap size for the JVM to 16GB. This limits how much memory the Minecraft server can use.

-Xms16G:
This sets the initial heap size to 16GB. This allocates 16GB of RAM from the start, ensuring the server has that memory available right away.

Garbage Collection (GC) Optimization Flags

-XX:+UseG1GC:
Enables the G1 garbage collector. G1 is optimized for low-latency garbage collection and is recommended for applications that need to handle large heaps like Minecraft.

-XX:+ParallelRefProcEnabled:
Enables parallel reference processing during garbage collection, improving GC performance by handling reference objects in parallel.

-XX:MaxGCPauseMillis=200:
Sets a target for the maximum pause time for garbage collection to 200 milliseconds. This means the JVM will try to keep GC pauses under 200ms to avoid impacting performance.

-XX:+UnlockExperimentalVMOptions:
Unlocks experimental JVM options. This allows the JVM to use advanced and less commonly used optimizations.

-XX:+DisableExplicitGC:
Disables calls to System.gc() from the code, preventing explicit garbage collection that might affect performance.

-XX:+AlwaysPreTouch:
Pre-touch memory pages during JVM startup, ensuring all memory is allocated and locked upfront, which can reduce pauses during runtime.

G1 Garbage Collection Tuning Flags

-XX:G1NewSizePercent=40:
Sets the minimum size of the new (young) generation to 40% of the total heap.

-XX:G1MaxNewSizePercent=50:
Sets the maximum size of the new (young) generation to 50% of the total heap.

-XX:G1HeapRegionSize=16M:
Sets the size of each heap region in G1 garbage collection to 16MB. Larger region sizes are better for large heaps.

-XX:G1ReservePercent=15:
Reserves 15% of the heap as free space to reduce the chance of full garbage collection cycles (which are more expensive).

-XX:G1HeapWastePercent=5:
Sets the tolerated heap waste percentage. G1 will aim to reclaim regions if more than 5% of the heap is considered "waste."

-XX:G1MixedGCCountTarget=4:
Sets the target number of mixed garbage collections (which reclaim both old and young regions) to 4.

-XX:InitiatingHeapOccupancyPercent=20:
Sets the threshold for starting concurrent garbage collection at 20% heap occupancy. This allows GC to start early enough to avoid larger full GC cycles.

-XX:G1MixedGCLiveThresholdPercent=90:
G1 will not reclaim any old regions where more than 90% of the region contains live objects.

-XX:G1RSetUpdatingPauseTimePercent=5:
Limits the pause time for updating the remembered set (RSet) to 5% of the GC pause time.

-XX:SurvivorRatio=32:
Sets the ratio of Eden to Survivor spaces in the young generation to 32:1. Larger ratios mean more space in Eden, reducing promotion to the old generation.

-XX:+PerfDisableSharedMem:
Disables the use of shared memory for performance monitoring, which can prevent unnecessary memory overhead.

-XX:MaxTenuringThreshold=1:
Sets the maximum tenuring threshold to 1. This determines how many garbage collection cycles an object will go through before being promoted to the old generation.

Other Flags

--add-modules=jdk.incubator.vector:
Adds support for incubator modules in Java, such as the vector API. Incubator modules are experimental features.

-jar /home/minecraft/purpur/purpur-1.21.1-2303.jar:
Specifies the path to the Minecraft server JAR file to be executed.

--nogui:
Disables the Minecraft server's GUI for performance reasons since the server is run headless.

Old / Legacy Notes

I used to not use systemd and run Purpur with a shell script. here's what that old file would look like:

#!/bin/bash

JAVA="java"
JAR="purpur-1.21.1-2303.jar"
RAM="16000M"
FLAGS="-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=40 -XX:G1MaxNewSizePercent=50 -XX:G1HeapRegionSize=16M -XX:G1ReservePercent=15 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=20 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1"

echo "Starting the Minecraft server..."
${JAVA} -Xmx${RAM} -Xms${RAM} ${FLAGS} --add-modules=jdk.incubator.vector -jar ${JAR} --nogui

Troubleshooting

If you run into file or folder permissions issues, do this to correct them:

sudo find /home/minecraft/purpur/ -type d -exec chmod 755 {} \;

sudo find /home/minecraft/purpur/ -type f -exec chmod 644 {} \;

But remember the .jar purpur file must always be executible

sudo chmod +x purpur-1.21.1-2303.jar

Audit your systemd service security with:

systemd-analyze security minecraft.service