Published:

Table of Contents

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!