4.2 KiB
mc-packwiz-deploy
Self-hosted NeoForge Minecraft server with a packwiz modpack that clients pull automatically on every launch. Mods are served from an HTTP mirror of the server's own mods directory — no Modrinth or CurseForge dependency.
Everything lives on a ZFS dataset mounted at /minecraft.
Quick start
./deploy.sh -n "Opengames" -a themiro \
-u https://games.abdelbaki.eu/mods \
--accept-eula
The NeoForge version is resolved automatically — the newest stable build in the
series matching your Minecraft version, from the NeoForged maven. Pin one with
-N 21.1.248 if you'd rather not track latest.
Then:
sudo systemctl start minecraft
journalctl -u minecraft -f
Send the players https://<your-host>/setup-windows-packwiz.html or
setup-linux-packwiz.html. Those are generated with your real pack URL and
server address already filled in.
What's in here
| Script | Runs as | Does |
|---|---|---|
deploy.sh |
you | Runs the two setup scripts in order |
mc-service-setup.sh |
root | minecraft user, NeoForge server, systemd units |
packwiz-setup.sh |
you | packwiz install, pack init, mod index, player guides |
mc-refresh-restart.sh |
you | Re-sync the pack after changing mods, then restart |
build.sh |
you | Builds the release zip |
Memory
The heap defaults to 10 GB (-X/-x to change it). -XX:+AlwaysPreTouch means
the JVM commits the whole heap at startup rather than growing into it, so the
number you set is the number actually taken.
On a ZFS host, cap the ARC to go with it — ZFS defaults to using up to half of RAM for cache, and will otherwise contend with the JVM for the same pages:
echo "options zfs zfs_arc_max=2147483648" | sudo tee /etc/modprobe.d/zfs.conf
sudo dracut --force && sudo reboot
That pins the ARC to 2 GB. On a 16 GB box that leaves roughly 4 GB for the OS and anything else the host runs, which is comfortable.
Requirements
- Fedora (or any systemd distro;
dnfcalls need swapping otherwise) - A ZFS dataset mounted at
/minecraft - Java 21
- A web server already publishing
/minecraft—/modsand/packsmust be reachable over HTTP
Every script refuses to run if /minecraft isn't a mountpoint. An unmounted
dataset leaves it as an empty directory on the root filesystem, and writing a
world or a mod mirror there fills the root disk instead of the pool.
Day-to-day
Add or remove a jar in /minecraft/mods, then:
./mc-refresh-restart.sh
That registers new mods, re-pins any whose file changed, drops metadata for deleted ones, takes a ZFS snapshot, restarts the server, and prunes to the 8 most recent snapshots. Players get the change on their next launch.
It refuses to restart if any mod failed to register, so the server never comes
up on a half-synced pack. Edit the config block at the top first — PACK_DIR,
BASE_URL, and PACK_URL need to match your deployment.
The server unit
minecraft.service pairs with minecraft.socket, a console FIFO at
/run/minecraft-console. That's what makes a clean shutdown possible:
sudo systemctl stop minecraft # sends "stop", world saves properly
echo "say hello" | sudo tee /run/minecraft-console
journalctl -u minecraft -f # console output
Without the FIFO, systemd would SIGTERM the JVM and risk cutting a world save in half.
Web server
/minecraft needs to be served over HTTP. Serve pack.toml and index.toml
with Cache-Control: no-cache — a caching proxy will otherwise hand clients a
stale pack while the server runs the new one. mc-refresh-restart.sh checks for
exactly that and stops before restarting if it finds it.
Caveats
packwiz-setup.shprobespackwiz --helpat runtime rather than assuming flag names, since the NeoForge options have moved between builds. If it bails with an interface complaint, the installed packwiz differs from what's expected.- Mod names come from filenames with the trailing version stripped
(
create-1.21.1-6.0.4.jar→create). It's a heuristic; rename the generated.pw.tomlfiles if you don't like the result. - Hashes are pinned. Replacing a jar in place under the same filename requires a re-sync, or clients fail the hash check.