A redeploy onto a host where earlier runs failed partway had no way to recover:
the obsolete minecraft.socket stayed, a hand-made `systemctl edit` drop-in kept
overriding ExecStart, units sat in a failed state, and a stale FIFO lingered.
mc-service-setup.sh now clears that up front: stops both services, removes the
socket unit, moves any minecraft.service.d / packwiz-http.service.d drop-in
aside with a timestamp (moved, not deleted, in case one was deliberate), removes
stale FIFOs at both the old and current paths, resets failed state and reloads.
It also gives the pack and the generated guides back to the admin user, undoing
the blanket chown an earlier version applied on every run.
START_SERVER now defaults to yes, so unpacking the zip and running install.sh
ends with a running server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying from removable media needed a single entry point that does not
require remembering a long flag line at a console.
install.sh reads deploy.conf, installs missing prerequisites with dnf, checks
that the share is actually mounted, optionally caps the ZFS ARC, runs the
deploy and can start the server. It copies itself off read-only media to the
home directory first, since the deploy needs somewhere writable. --check
validates the machine and changes nothing.
ACCEPT_EULA must be set to yes in the config; the installer will not infer it.
build.sh --iso produces a bootable-media image via xorrisofs/genisoimage/
mkisofs, carrying a ready-to-edit deploy.conf and a README.FIRST.txt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The socket declared Service=minecraft.service and PartOf=minecraft.service
while the service declared Requires=minecraft.socket. That is a cycle, which
systemd breaks by failing a job — surfacing as "a dependency job for
minecraft.service failed" with no obvious cause.
The socket now only listens; the service owns the relationship via
Requires=/After=/Sockets=. Also stop enabling minecraft.socket directly: it has
no [Install] section, and the service pulls it in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mc-service-setup.sh ran chown -R minecraft:minecraft over the whole share on
every invocation. That is right for first-time setup and wrong afterwards: it
takes the generated guides and the pack away from the user who authors them, so
the next packwiz-setup.sh run could not overwrite files it had written itself.
The directory stayed writable, so the up-front -w check passed and the failure
surfaced as a bare "Permission denied" mid-write.
The blanket chown now runs only when the service user is created in that same
run. libraries/ is still chowned unconditionally, since the NeoForge installer
writes it and the service must own it.
packwiz-setup.sh also removes a guide it cannot write before regenerating it,
rather than truncating in place, so a share left in the old state repairs itself
instead of needing manual chown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things stopped a from-scratch deploy finishing in one run.
The mirror reachability probe ran before counting jars, so a fresh instance
with an empty mods directory still failed on an unreachable mirror despite
having nothing to fetch. The probe now runs only when there is at least one jar,
which also means the reverse proxy does not have to be in place yet to build the
initial pack. mc-refresh-restart.sh gets the same treatment: zero jars is a
legitimate state, and the orphan sweep clears the metadata to match.
The probe itself never worked. curl -w '%{http_code}' already prints 000 on a
connection failure, so the trailing "|| echo 000" appended a second one and the
value was 000000, matching neither the unreachable case nor anything else — the
guard silently degraded to a warning. Use "|| true" with a :-000 fallback.
Verified against a refused port and a live host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On a first deploy the server has not run yet, so /minecraft/mods does not
exist and packwiz-setup.sh died before creating anything. It now creates the
directory, and mc-service-setup.sh also creates it up front with setgid so jars
dropped in by either the service user or the admin stay group-readable.
An empty mods directory is likewise no longer fatal. A pack with no mods yet is
a valid starting point: the pack, the index and the player guides are all
generated, and jars registered later with mc-refresh-restart.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reverse proxy runs on a different host than the game server, so it cannot
alias the share directly — serving the pack needs an HTTP server on the game
server itself.
packwiz-http.py is a threaded static server over an allowlist: mods/, packs/,
and setup-*.html. Everything else 404s, since the same directory holds
server.properties, ops.json, whitelist.json, logs and the world. Threading
matters because installing a pack is one request per mod and a single-threaded
server serialises a whole lobby behind one download. pack.toml and index.toml
are sent no-cache so a proxy cannot serve a stale pack.
Installed as packwiz-http.service on port 18080 by default, configurable with
-H/-B, skippable with --no-http, and read-only via ReadOnlyPaths.
packwiz-setup.sh gains -S/--scheme, for when the pack is built against a mirror
reachable only over plain HTTP but clients must be handed the public HTTPS URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deriving the site URL by reducing the mirror URL to its origin discarded any
base path, so a mirror at https://host/abdelpakDelta/mods produced a pack URL
of https://host/packs/<slug> — a location nothing serves. Strip the last path
segment instead, which gives the right answer with or without a base path.
The server address derivation also only stripped scheme and port, so a site URL
carrying a path leaked it into the address shown to players. Strip the path too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-running previously died at step 2, because packwiz-setup.sh refused to
overwrite an existing pack.toml — so a run that failed partway, or any later
change to port or heap, could not be applied by simply running it again.
deploy.sh now snapshots the dataset, stops the server if it was running,
redeploys, and restarts it only if it had been up. packwiz-setup.sh gains
--force, which moves an existing pack to <pack>.bak.<timestamp> rather than
deleting it, since the directory may hold hand-edited .pw.toml files or git
history. deploy.sh passes --force unless --keep-pack is given.
World data and the server's mods directory are untouched throughout.
Also replaces the hardcoded sed line ranges in every usage() with an awk that
prints the leading comment block, so help text stops drifting out of sync when
the header changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Threads a port through all three scripts. mc-service-setup.sh writes
server-port into server.properties (updating in place so a generated file keeps
its other settings) and opens the port in firewalld. packwiz-setup.sh renders it
into the player guides.
The guides print a bare hostname on 25565 and host:port otherwise — Minecraft
assumes the default, and a needless ":25565" in the address reliably confuses
players.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13G left roughly 1G for the OS once the ZFS ARC cap is accounted for. 10G plus
a 2G ARC leaves comfortable headroom on a 16G host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mktemp -d creates a 0700 directory owned by root, so handing the downloaded
installer jar to the minecraft user via runuser failed with "Unable to access
jarfile". Relax the temp path to 0755/0644 — the installer is a public
download and holds nothing sensitive.
Also set HOME explicitly for the runuser invocation, since it otherwise
inherits root's and the installer writes a cache alongside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
deploy.sh previously required -N and failed outright without it. It now passes
-N latest by default, and mc-service-setup.sh resolves the newest stable build
in the series matching the Minecraft version from the NeoForged maven
(1.21.1 -> 21.1.x, 1.21 -> 21.0.x), excluding betas. Pin a version with -N to
opt out.
Heap defaults raised to 13G for both Xms and Xmx. README documents capping the
ZFS ARC alongside it, since ARC defaults to half of RAM and would otherwise
contend with a heap this size on the same box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploys a self-hosted NeoForge server whose modpack clients pull automatically
on launch, with mods served from an HTTP mirror of the server's own mods dir
rather than Modrinth or CurseForge.
- mc-service-setup.sh: minecraft user, NeoForge install, systemd service paired
with a console FIFO socket so stops are graceful and the world saves
- packwiz-setup.sh: packwiz install, pack init, mod index built from the
mirror, and per-OS player setup guides generated with real URLs baked in
- mc-refresh-restart.sh: re-sync after mod changes, ZFS snapshot, restart
- deploy.sh: runs the above in order
- build.sh: release zip, refusing to package scripts that don't parse
Every entry point refuses to run when /minecraft is not a mountpoint, so an
unmounted dataset cannot silently fill the root filesystem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>