feat(mods): replace packwiz with distribution-fed custom mod volume

Drop packwiz entirely. The server now loads a filtered mod subset from a
local ./server-mods volume instead of fetching a packwiz pack at boot.

New flow:
- tooling/classify-mods.py (tomllib only, no network) reads each jar's
  META-INF/neoforge.mods.toml and seeds mods-sides.json with a side
  (client|server|both; default both). Existing entries are preserved.
- mods-sides.json: committed, keyed by jar filename, hand-editable override
  surface. Initial seed: 65 both, 11 client, 0 server (76 jars).
- tooling/sync-server-mods.sh mirrors the both/server jars from
  $DISTRIBUTION_WEB_ROOT into ./server-mods/ (authoritative; prunes strays;
  halts on a missing jar). Replaces render-pack.sh.
- compose: minecraft mounts ./server-mods:/mods:ro with REMOVE_OLD_MODS=TRUE
  (itzg auto-syncs /mods -> /data/mods). Removed PACKWIZ_URL, the pack.
  extra_host (auth. kept for authlib), and depends_on caddy (drasl kept).

Both classify-mods.py and sync-server-mods.sh resolve their source dir as:
CLI arg / MODS_DIST_DIR env / $DISTRIBUTION_WEB_ROOT (in that order).

Removed: tooling/render-pack.sh + add-custom-mod.sh (packwiz tooling),
pack/ (packwiz source), custom/ (76 jars now sourced from the distribution),
the pack. caddy vhost + its mounts/alias, and the packwiz .gitignore block.

Client distribution is UNCHANGED: HeliosLauncher still installs the full
modset from distribution.json. Docs (CLAUDE.md, plan/04/05/14, runtime)
updated; plan/04-packwiz.md stubbed as superseded by plan/04-mods.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:34:38 +02:00
parent 90c3be7bb5
commit 27237bc7c1
108 changed files with 647 additions and 369 deletions

View File

@@ -3,9 +3,9 @@
# ──────────────────────────────────────────────────────────────────
# Single, unified compose. One file holds the whole stack:
# drasl auth + skins (Yggdrasil), internal only
# minecraft the server (itzg/NEOFORGE), installs mods via packwiz
# minecraft the server (itzg/NEOFORGE), mods from ./server-mods volume
# mc-backup world backups every 6h via RCON
# caddy internal ingress — routes auth./pack./apex/launcher/avatar.
# caddy internal ingress — routes auth./apex/launcher/avatar.
# /distribution. by Host header (conf.d/*.caddy)
# nmsr skin/avatar renderer at avatar.${BASE_DOMAIN}
# uptime-kuma status monitoring at status.${BASE_DOMAIN}
@@ -16,7 +16,7 @@
# Production TLS terminates at the HOST's nginx in front of caddy
# (nginx/ulicraft-caddy.conf.tmpl + Let's Encrypt); caddy is published on a
# localhost-only port (CADDY_HTTP_BIND/CADDY_HTTP_PORT in .env). Containers reach
# the stack's own names internally via caddy's mcnet aliases (auth./pack.).
# the stack's own names internally via caddy's mcnet alias (auth.).
# ──────────────────────────────────────────────────────────────────
services:
@@ -71,8 +71,12 @@ services:
ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+
ALLOW_FLIGHT: "TRUE" # many tech mods need this
# ── Mod source: packwiz pack served by caddy (alias pack.) ──
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
# ── Mod source: filtered subset of the distribution modset ──
# Jars are synced into ./server-mods by tooling/sync-server-mods.sh
# (both/server entries from mods-sides.json) and mounted at /mods below;
# itzg auto-syncs /mods → /data/mods. REMOVE_OLD_MODS makes that mirror
# authoritative so removed mods get pruned. See plan/04-mods.md.
REMOVE_OLD_MODS: "TRUE"
# ── RCON for remote admin ───────────────────────────────────
ENABLE_RCON: "TRUE"
@@ -82,16 +86,15 @@ services:
TZ: "Europe/Madrid"
volumes:
- mc_data:/data
- ./runtime:/extras:ro # authlib-injector.jar lives here
- ./runtime:/extras:ro # authlib-injector.jar lives here
- ./server-mods:/mods:ro # itzg auto-syncs /mods → /data/mods
depends_on:
- drasl
- caddy # pack./auth. must resolve at boot
- drasl # authlib (auth.) must resolve at boot
# Containers don't inherit the host's /etc/hosts; the LAN resolver returns a
# dead address for the public names. Pin auth./pack. to the host so HTTPS to
# them lands on the host's nginx (valid LE cert) -> caddy -> service.
# dead address for the public names. Pin auth. to the host so HTTPS to it
# lands on the host's nginx (valid LE cert) -> caddy -> drasl.
extra_hosts:
- "auth.${BASE_DOMAIN}:host-gateway"
- "pack.${BASE_DOMAIN}:host-gateway"
networks:
- mcnet
@@ -114,7 +117,7 @@ services:
- mcnet
# Internal ingress. Routes every vhost by Host header (conf.d/*.caddy):
# auth. -> drasl, pack. -> packwiz files, apex -> landing + /launcher,
# auth. -> drasl, apex -> landing + /launcher,
# avatar. -> nmsr, status. -> uptime-kuma, distribution. -> static site.
# Published on a localhost-only port; the host's nginx terminates TLS in
# front of it (nginx/ulicraft-caddy.conf.tmpl).
@@ -135,13 +138,6 @@ services:
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
- ./www:/srv/www:ro
- ./launcher:/srv/launcher:ro
- ./pack:/srv/pack:ro
# Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh
# doesn't index them as direct files. Served at pack.${BASE_DOMAIN}/custom/.
# Nested under the :ro /srv/pack mount — the mountpoint pack/custom/ must
# pre-exist on the host (tracked via pack/custom/.gitkeep) or Docker can't
# mkdir it in the read-only parent.
- ./custom:/srv/pack/custom:ro
# Static site from ANOTHER repo (absolute host path in .env).
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}:/srv/distribution:ro
networks:
@@ -149,7 +145,6 @@ services:
# Containers resolve the stack's own public names to caddy internally.
aliases:
- "auth.${BASE_DOMAIN}"
- "pack.${BASE_DOMAIN}"
# Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls
# player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080.