Table of Contents
- Introduction
- Install dependencies
- Install Java
- Create an isolated Linux user for security
- Download the Curseforge Modpack
- Download Forge
- Download the modpack's mods
- Deleting client-side mods
- Copy over the modpack configs
- Install and test the server
- Create a systemd service for security and ease of use
- Firewalling Minecraft
- Tuning the JVM arguments
- Troubleshooting
- Maintenance
- Custom JVM arguments explained
Introduction
Got feedback? email feedback at yawnbox dot is
This guide aims to document how to create a modded Minecraft linux server from a Curseforge modpack that does not have server files. My example uses a linux server on the internet with public IPs. If you're a modpack dev trying to make server files for your Curseforge page, you may be able to adapt this guide, but this guide is not written for you.
In my example for the purposes of writing this, I'm testing An Inconvenient Modpack which uses Forge 1.18.2.
I'm using Ubuntu Server 22.04 LTS. You need to have root/sudo privileges and local or ssh access to your server.
Install dependencies
In my work below, I depend on unzip and jq. As a privileged (sudo) user:
sudo apt install unzip jq -V
Install Java
I believe you can use any version of Java that you like, but I recommend using the latest Adoptium's Eclipse Temurin v21. However I use the bleeding edge v23 and it works great too.
Installation documentation: https://adoptium.net/installation/
v21, as a privileged (sudo) user:
sudo apt install temurin-21-jdk
v23 (see https://adoptium.net/temurin/releases/?version=23), as a privileged (sudo) user:
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-23-jdk zip -V
Validate the Java version:
/usr/bin/java --version
The output should look like:
openjdk 23 2024-09-17
OpenJDK Runtime Environment Temurin-23+37 (build 23+37)
OpenJDK 64-Bit Server VM Temurin-23+37 (build 23+37, mixed mode, sharing)
Create an isolated Linux user for security
As a privileged (sudo) user, create a limited non-privileged user called "minecraft" 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, i refer to this folder as my "working folder" for the majority of this guide:
sudo mkdir /home/minecraft/inconvenient
Now change to your new minecraft user:
sudo su minecraft
Go to the working directory:
cd /home/minecraft/inconvenient
Download the Curseforge Modpack
As of writing, An Inconvenient Modpack (my example) is at version 0.6.10. Find the download link from your modpack's Curseforge page.
wget https://mediafilez.forgecdn.net/files/5802/672/An%20Inconvenient%20Modpack%200.6.10.zip
unzip An\ Inconvenient\ Modpack\ 0.6.10.zip
Download Forge
An Inconvenient Modpack (my example) depends on Forge 1.18.2. Determine which Forge version you need. In a web browser, go to: https://files.minecraftforge.net/net/minecraftforge/forge/index_1.18.2.html. Note: I have not tested this with NeoForge.
You will see an "Installer" link under "Download Latest". Right-click that "Installer" link and click "Copy Link".
The link you copy will look like:
https://adfoc.us/serve/sitelinks/?id=271228&url=https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.2.21/forge-1.18.2-40.2.21-installer.jar
Delete the first part of that link. They add it for link tracking and it breaks a simple wget download on linux:
https://adfoc.us/serve/sitelinks/?id=271228&url=
The second part of the original link is what you need:
https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.2.21/forge-1.18.2-40.2.21-installer.jar
Download the jar file:
wget https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.2.21/forge-1.18.2-40.2.21-installer.jar
Make the jar file executable:
chmod +x forge-1.18.2-40.2.21-installer.jar
Download the modpack's mods
The Curseforge modpack zip file doesn't come with the mods, it comes with a "manifest.json" file with a list of mods that need to be downloaded. I made a shell script to parse the "manifest.json" file from any modpack to automatically create the mods folder then download all (200+ in my case) mods for the modpack.
Create the shell script file. I use vim, you can use nano, etc:
vim download-mods.sh
Paste in this shell script:
#!/bin/bash
# Ensure jq is installed
if ! command -v jq &> /dev/null; then
echo "jq could not be found. Please install it using your package manager."
exit 1
fi
# Create mods directory if it doesn't exist
mkdir -p mods
# Loop through each mod in manifest.json
jq -r '.files[] | .downloadUrl' manifest.json | while read -r url; do
if [ -n "$url" ]; then
# Extract filename from the URL
filename=$(basename "$url")
# Show download progress
echo -e "\nDownloading: $filename"
# Download the file with minimal curl output
curl -L -o "mods/$filename" "$url" --silent --show-error
# Check if the download succeeded
if [ $? -eq 0 ]; then
echo "✓ Downloaded $filename successfully"
else
echo "✗ Failed to download $filename"
fi
else
echo "No download URL found for a mod entry."
fi
done
echo -e "\nAll downloads completed."
Save and quit vim:
:wq
Make the script executable:
chmod +x download-mods.sh
Run the script to download the mods:
./download-mods.sh
Once the download is complete, you can see all of the mods with:
ls -alh mods/
If you need to update one or more mods, it's easiest to just delete the whole mods folder then redownload everything with the same manifest download script.
rm -rf mods/
./download-mods.sh
Be sure to delete the client-side mods!
Deleting client-side mods
Deleting client-side mods is critical for server files. If there are client-only mods in a server, the server probably won't launch. There's lots of ways to do this, but there's not one great answer. Not all mods clearly indicate on the Curseforge page that they are or are not client-side or server-side mods. I've had to resolve this with trial and error. Later on in this guide, once the server is in a startable/stoppable state, you can read the error logs to see which mods are client-side only then delete them from the mods folder.
For example, Oculus is a client side mod in An Inconvenient Modpack that needs to be deleted. The server won't start with it:
rm /home/minecraft/inconvenient/mods/oculus-*.jar
Copy over the modpack configs
After the earlier step where we unzipped the modpack, an "overrides" folder was created in our working directory. Copy its contents into our working directory:
cp -r overrides/* .
Install and test the server
Install the base server files:
java -jar forge-1.18.2-40.2.21-installer.jar --installServer
Edit the server properties file that gets created:
vim server.properties
In server.properties, the "level-name=world" line tells the server where the world folder/files are. For example, in An Inconvenient Modpack that has a pre-built world, that world folder is "template". So for me, I changed the level-name to "level-name=template".
Other things I typically change in server.properties:
motd=your.domain.com
difficulty=hard
max-players=9000
allow-flight=true
view-distance=20
server-ip=1.2.3.4
server-name=your.domain.com
level-type=AMPLIFIED
Accept the EULA:
rm -f eula.txt && touch eula.txt && echo 'eula=true' >> eula.txt && cat eula.txt
Run the server for the first time:
./run.sh
Presuming the server runs without errors, once it launches fully, type stop to stop the server
stop
If the server has errors and it hasn't full started, you'll need to stop the process with ctrl + c keys on your keyboard, then figure out what went wrong.
If you have errors, I can't help you, so please don't ask for help. They are either client-side mod errors, or you need to report the errors either to the owner of the modpack or to the owner of the respective mod. Java errors are frustratingly unhelpful so I am very sorry if you have to deal with them.
Create a 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 of Java possible for security and performance reassons, I use a hardened systemd service to manage Java Minecraft.
As a privileged (sudo) user, 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=Modded Minecraft Server
After=network.target
[Service]
User=minecraft
Group=minecraft
# Restrict local network access to local 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
NoNewPrivileges=true
ProtectSystem=full
ProtectClock=true
PrivateDevices=true
ProtectKernelLogs=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/inconvenient
WorkingDirectory=/home/minecraft/inconvenient
# JVM flags for performance optimization
ExecStart=/home/minecraft/inconvenient/run.sh --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 server's 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/inconvenient
WorkingDirectory=/home/minecraft/inconvenient
^ These two paths are the same, as they are where the modpack is downloaded to.
ExecStart=/home/minecraft/inconvenient/run.sh --nogui
^ This line clearly shows the run.sh file in our working directory. This is how the modpack will launch. If you want to tune the JVM arguments, I share how to do that later in this guide.
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
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
Firewalling Minecraft
Be sure that you understand Ubuntu's firewall, ufw, and if its already enabled on your server before you make any changes. You don't want to lock yourself out of your server, like if you're not already allowing 22/tcp for ssh.
View existing rules, as a privileged (sudo) user:
sudo ufw status verbose numbered
If it's not active, then you don't need to open up the port. But i'd urge you to learn about ufw and start using it if you're not.
Presuming you have already enabled the firewall for server security, open up the default Minecraft port. I use "limit" instead of "allow" to prevent DoS attacks:
sudo ufw limit 25565/tcp
To apply the firewall change:
sudo ufw reload
Tuning the JVM arguments
While still in /home/minecraft/inconvenient:
vim user_jvm_args.txt
In order to maximize Java performance, i set these arguments. I've used these from Java versions 17 - 23. Add these to the bottom of this file:
-Xms16G
-Xmx16G
-XX:+UnlockExperimentalVMOptions
-XX:+DisableExplicitGC
-XX:+AlwaysPreTouch
-XX:+PerfDisableSharedMem
-XX:+UseG1GC
-XX:+ParallelRefProcEnabled
-XX:MaxGCPauseMillis=200
-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:MaxTenuringThreshold=1
The -Xms16G and -Xmx16G lines combined tell the server to use a static 16GB of RAM. I prefer this since my server has 64GB of RAM. Change these flags based on how much starting RAM and how much max RAM you want to give the Minecraft server. The other arguments are mostly for Java garbage collection. For descriptions of these java arguments, see the bottom of this article.
I haven't tried this, but you may be able to log G1GC output with:
-Xlog:gc*,gc+heap,gc+region,gc+pause=debug:file=/home/minecraft/inconvenient/logs/gc_full.log:time
If you're using Java version 22 or 23 like me, you can take advantage of newer garbage collection arguments called Generational ZGC. Use these arguments instead:
-Xms16G
-Xmx16G
-XX:+UnlockExperimentalVMOptions
-XX:+DisableExplicitGC
-XX:+AlwaysPreTouch
-XX:+PerfDisableSharedMem
-XX:+UseZGC
-Xlog:gc*:file=/home/minecraft/inconvenient/logs/zgc.log
Now you can also see Java garbage colelction logs!
tail -f /home/minecraft/inconvenient/logs/zgc.log
Troubleshooting
Avoid doing things with sudo in your Minecraft working directory. If you need to fix permissions:
sudo chown minecraft:minecraft -R /home/minecraft/
Maintenance
To start:
sudo systemctl start inconvenient-modpack.service
To stop:
sudo systemctl stop inconvenient-modpack.service
To restart:
sudo systemctl restart inconvenient-modpack.service
To see the status:
sudo systemctl status inconvenient-modpack.service
To view logs:
sudo journalctl -u inconvenient-modpack.service
or
cat /home/minecraft/inconvenient/logs/latest.log
or
tail -f /home/minecraft/inconvenient/logs/latest.log
To backup a world folder manually:
zip -r /home/minecraft/inconvenient/world.zip /home/minecraft/inconvenient/world/
To delete a world and start over (while the server is stopped):
rm -r /home/minecraft/inconvenient/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
If you want to tune the security of your new systemd service:
sudo systemd-analyze security minecraft.service
Custom JVM 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/server.jar:
Specifies the path to the Minecraft server JAR file to be executed, if used.
--nogui:
Disables the Minecraft server's GUI for performance reasons since the server is run headless.
Have fun!