Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How the servers are set up

Three Fabric servers, one public port, and one modpack per world that is both what the server runs and what players install.

The pieces

PieceWhereDoes
mc-routermodules/containers/mc-router.nixowns 25565, reads the hostname the player typed, routes to that world, wakes it if asleep
minecraft-scalermodules/containers/minecraft-cap.nixturns mc-router’s wake/sleep webhook into systemctl start/stop; refuses a third world
the worldsmodules/containers/minecraft.nixone itzg/minecraft-server container each, on its own volume, RCON on loopback only
the packspacks/mc1 to packs/mc4each world’s mods, pinned, with the side each one runs on
the pack buildpacks/mrpack.nixturns a pack into one .mrpack, plus the dependency check
the downloads.forgejo/workflows/pages.ymlpublishes the same .mrpack files on pages

Waking, sleeping, the cap of two and the whitelist-gated wake are described in Worlds on demand; the heap settings in Why the Minecraft heap is two numbers.

One pack, two consumers

packs/mc1/                 a packwiz pack: one .pw.toml per mod, each pinning an
  pack.toml                exact Modrinth version, its sha512 and its side
  mods/*.pw.toml
  config/...               client config files
      │
      ▼  packs/mrpack.nix
mc.hu-tao.dev-26.1.2.mrpack    named <world>.hu-tao.dev-<minecraft version>
      ├──▶ mounted into the world's container; itzg installs it (TYPE=MODRINTH)
      └──▶ published on pages; players import it into their launcher

Because both come from one build of one directory, the server and the players’ pack cannot disagree about a mod both sides need. Before this, world 2’s server picked the newest build of each mod at every start while players kept a pack from weeks earlier; when the packs were introduced, the zombie-horde mod was at 2.0.0 on the server and 1.2.0 in the players’ pack.

The .mrpack format

A .mrpack is a zip holding modrinth.index.json (every file, with its download URL, sha1, sha512, size and which sides need it) and optional overrides/, client-overrides/ and server-overrides/ folders copied into the game directory. The specification is Modrinth’s: Modrinth Modpack format (.mrpack).

Two of its fields carry this setup:

  • env on each file says whether the client and the server need it. itzg skips every file marked server: unsupported (checked in mc-image-helper’s FileInclusionCalculator), so client-only mods such as Sodium never reach a server.
  • client-overrides/ is where the packs’ config files go. itzg extracts overrides/ and server-overrides/ into the server’s /data, but never client-overrides/, so a player’s lithium.properties cannot quietly reconfigure a live server. Server config is the server’s own.

packwiz

The packs are kept with packwiz, which stores a pack as plain TOML files that diff well and does the version work: adding a mod with its dependencies, updating everything, and moving to a new Minecraft version. How to use it here is in Working on the packs.

packwiz is not what builds the .mrpack, though. packwiz modrinth export downloads every file to hash it and, when a download fails, drops that file and still exits 0; inside the Nix sandbox every download fails, and the result was a valid-looking pack with no mods in it. packs/mrpack.nix fetches each file itself, pinned by the sha512 packwiz recorded, so a dead link or a changed file fails the build instead. The first build on a machine downloads every file in the pack (about 300 MB for mc3), then they are in the Nix store.

Byte-for-byte reproducible

The .mrpack is built with a sorted file list, fixed modes and timestamps and no extra zip fields, so the same commit gives the same bytes on any machine. That makes “is the download the same as the server’s pack” a hash comparison. It was not at first: zip -r takes files in the filesystem’s directory order, which differed between CI and a desktop.

The dependency check

checks.x86_64-linux.mc<N>-pack opens every mod jar in the pack, including the jars nested inside other jars, and checks every hard depends in its fabric.mod.json against what each side actually gets. Modrinth’s dependency data is not enough: world 2’s first start failed on geckolib, which vics-point-blank requires in its jar and declares nowhere on Modrinth. A mod whose library is server-only crashes every player’s launch, and the reverse stops the server, so both sides are checked. It runs offline, on the same pinned downloads the pack is built from, as a pre-commit hook and in CI.

Version ranges are not evaluated, only presence: a mismatch names itself in the server log, while a missing mod is a crash on every player’s launch.