Go to file
Amir Alexander Abdelbaki 6c85e30cc8 Fix console FIFO blocked by SELinux
minecraft.socket had systemd open the FIFO, which SELinux refuses:

  avc: denied { read write } for pid=1 comm="systemd" name="minecraft-console"
  scontext=...:init_t:s0 tcontext=...:var_run_t:s0 tclass=fifo_file

init_t has no read/write on a var_run_t fifo_file under the stock Fedora
targeted policy, so the socket unit never started and the service failed with
"a dependency job for minecraft.service failed".

The socket unit is gone. The service now creates the FIFO in its own
RuntimeDirectory and opens it as fd 3 read-write before exec'ing the JVM, so
the open never blocks, the reader never sees EOF when a writer disconnects, and
MAINPID stays the JVM. Nothing goes through PID 1, so the policy gap does not
apply. An obsolete minecraft.socket is removed on upgrade.

Console path moves to /run/minecraft/console.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:37:50 +02:00
__pycache__ Add packwiz-http service and --scheme flag 2026-08-05 23:27:40 +02:00
.gitignore Initial commit: packwiz + NeoForge hosting kit 2026-08-05 22:23:34 +02:00
README.md Fix console FIFO blocked by SELinux 2026-08-06 00:37:50 +02:00
build.sh Add packwiz-http service and --scheme flag 2026-08-05 23:27:40 +02:00
deploy.sh Add packwiz-http service and --scheme flag 2026-08-05 23:27:40 +02:00
mc-refresh-restart.sh Deploy a complete instance with no mods present 2026-08-05 23:38:57 +02:00
mc-service-setup.sh Fix console FIFO blocked by SELinux 2026-08-06 00:37:50 +02:00
packwiz-http.py Add packwiz-http service and --scheme flag 2026-08-05 23:27:40 +02:00
packwiz-setup.sh Stop re-runs from locking the author out of their own files 2026-08-06 00:09:14 +02:00

README.md

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

Add -p 25599 to run on a non-default port. It's written into server.properties, opened in firewalld, and shown in the player guides — which print a bare hostname on 25565 and host:port on anything else, since Minecraft assumes the default and a needless :25565 confuses people.

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.

Re-running

deploy.sh is safe to run repeatedly, and re-running is the intended way to apply changed settings — a port change, a different heap size, or a run that failed partway and left things half-applied.

Each run:

  1. Snapshots the dataset (<pool>@predeploy-<timestamp>)
  2. Stops the server if it was running
  3. Rewrites server.properties, user_jvm_args.txt and both unit files
  4. Moves any existing pack to <pack>.bak.<timestamp> and rebuilds it
  5. Regenerates the player guides
  6. Starts the server again if it had been running

The world and /minecraft/mods are never touched. The pack is moved aside rather than deleted, so hand-edited .pw.toml files and any git history in the pack directory survive.

--keep-pack skips the pack rebuild; --no-snapshot skips step 1.

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

Pack HTTP server

packwiz-http.service serves the pack, the mod jars and the setup guides on port 18080 (-H to change, -B to change the bind address, --no-http to skip it). Put a reverse proxy in front of it.

It serves an allowlist, not the whole share: mods/, packs/, and setup-*.html. Everything else 404s. That is deliberate — the same directory holds server.properties (which can carry an rcon password), ops.json, whitelist.json, usercache.json, logs and the world.

pack.toml and index.toml are sent with Cache-Control: no-cache, so the proxy can't hand clients a stale pack. Jars are pinned by hash in the index and cache freely.

If the mirror is only reachable over plain HTTP from the machine building the pack, but clients need the public HTTPS URL, use -S https — it rewrites the scheme on every generated URL.

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; dnf calls need swapping otherwise)
  • A ZFS dataset mounted at /minecraft
  • Java 21
  • A web server already publishing /minecraft/mods and /packs must 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 holds 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.

The service creates and opens the FIFO itself rather than delegating to a socket unit. SELinux denies init_t read/write on a fifo_file in /run, so anything routed through PID 1 fails with EACCES on a stock Fedora policy.

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.sh probes packwiz --help at 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.jarcreate). It's a heuristic; rename the generated .pw.toml files 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.