The download URL is the only domain-dependent field in the external-file
metadata. Make mods/*.pw.toml build output rendered from committed
mods/*.pw.toml.tmpl (url uses ${BASE_DOMAIN}); render-config.sh and
add-custom-mod.sh render them + packwiz refresh at deploy/add time.
- pack/.packwizignore: keep *.pw.toml.tmpl out of the index
- gitignore rendered pack/mods/*.pw.toml + pack/index.toml (build output)
- seed a minimal index.toml before refresh (packwiz won't bootstrap one)
- add-custom-mod renders inline (BASE_DOMAIN only) — no coupling to
dnsmasq's HOST_LAN_IP
Domain changes now just need a re-render, not a metadata rewrite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mods/*.pw.toml download URLs were frozen at ulicraft.lan; the stack
now uses ulicraft.net. Regenerate all nine with the current domain.
add-custom-mod.sh: skip the copy when source == hosted dest (`-ef`),
so `add-custom-mod.sh custom --force` works as an in-place regen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move pack/custom/ -> ./custom/ and bind-mount it into caddy at
/srv/pack/custom (served URL pack.${BASE_DOMAIN}/custom/ unchanged).
Keeping jars outside the packwiz pack root stops `packwiz refresh`
from indexing them as direct files on top of the mods/*.pw.toml
external refs (drops 9 bogus custom/*.jar entries from index.toml).
add-custom-mod.sh now writes jars to ./custom/. Trim the blessingskin
caddy override to just the conf swap (volumes merge by target).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nine NeoForge 1.21.1 jars hosted under pack/custom/ with hand-written
mods/*.pw.toml (JEI + JER + JEProfessions, Jade + JadeAddons, Balm,
InventoryEssentials, MouseTweaks, CustomSkinLoader). Adds
CustomSkinLoader.json config; refreshes index.toml/pack.toml.
Jars are committed because external files have no upstream URL — the
pack/custom/ copy served by caddy is the source of truth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the pack-serving vhost/alias from packwiz.${BASE_DOMAIN} to
pack.${BASE_DOMAIN} (caddy conf, caddy network alias, PACKWIZ_URL).
Inline the static-site caddy mounts (10-static.caddy, www, launcher,
CA cert) into the base compose; set MOTD to "Uli LAN party".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Accept a directory: process every *.jar inside and run packwiz refresh
once at the end. --name rejected with a folder (names derived per-jar);
halts on an empty folder. Single-jar path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copies a local jar under pack/custom/ (served by caddy), sha256s it,
writes pack/mods/<slug>.pw.toml per packwiz "Other external files",
then packwiz refresh. Cautious: halts on bad side, whitespace in
filename, or existing entry without --force.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Override stack that swaps Drasl for Blessing Skin Server + MariaDB,
talking to Minecraft via the yggdrasil-api plugin (authlib endpoint
/api/yggdrasil, ONLINE_MODE=TRUE). Caddy auth.* repointed to
blessing-skin:80; Drasl left idle (compose merges depends_on).
Run: docker compose -f docker-compose.yml -f docker-compose.blessingskin.yml up -d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dns-records.sh <mode> hosts prints a marker-wrapped /etc/hosts block to pipe
into /etc/hosts (single-machine testing without a DNS server). README documents
apply/remove + the airgap caveat (spoof hosts break real internet while present).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-detects the LAN interface from HOST_LAN_IP, sets allow-interfaces in
/etc/avahi/avahi-daemon.conf (idempotent, backs up), restarts avahi. Self-sudos.
Fixes the multi-bridge 'Local name collision' that blocks the mDNS path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When ENABLE_MDNS=true, warn if BASE_DOMAIN isn't .local, host avahi-daemon
isn't running, the D-Bus socket is missing, or the host has many interfaces
(avahi self-collision risk → suggests allow-interfaces).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A container running its own avahi-daemon collides with the host's on :5353.
Instead publish to the host avahi over its mounted D-Bus socket using avahi-tools
(avahi-publish). Needs apparmor=unconfined (docker-default blocks the D-Bus
publish). Documented host prereqs incl. allow-interfaces for multi-bridge hosts.
Tested: publish reaches host avahi and registers the names; on hosts with many
docker bridges avahi self-collides until restricted to the LAN interface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained host-network Avahi container publishes the service names over
mDNS so .local works zero-config for guests (macOS/Linux native, Windows needs
Bonjour). Toggle with ENABLE_MDNS=true + BASE_DOMAIN=*.local; build-stack layers
docker-compose.avahi.yml in and builds the image during --prep. mDNS can't serve
the air-gap upstream spoofs (non-.local) — those still need real DNS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DNS is now your own party server. tooling/dns-records.sh prints the records
(online = service names; airgap = + Mojang/launcher/NeoForge spoofs) in several
formats. build-stack --up online|airgap selects the mirror layer and prints the
matching records. dnsmasq moved to an optional override (docker-compose.dnsmasq.yml).
Default domain switched to .lan (.local is hijacked by mDNS on unicast DNS).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Air-gap mirror uses tls internal; guests must trust Caddy's local CA. Serve it
at http://<domain>/ca.crt from a world-readable mount (the live pki dir is
root-only 0600). build-stack --up mirror|full exports the CA after bring-up;
landing page links it. Pre-creates the mount target to avoid a dir bind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server JVM agent needs runtime/authlib-injector.jar (gitignored); pull the
latest yushijinhun release if absent so pre-bake doesn't crash-loop on a
missing agent jar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The spoof aliases (maven.neoforged.net, libraries.minecraft.net, etc.) hijacked
those hostnames for every mcnet container, so the ONLINE NeoForge install during
pre-bake hit the empty mirror (cert path failure). Moved them to the mirror
override; core/static modes resolve the real upstreams.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minecraft is a custom TCP protocol, not HTTP, so the no-port UX is solved with
a DNS SRV record (_minecraft._tcp.mc.<domain> -> :25565) rather than an HTTP
reverse proxy. Landing page now shows the bare address.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Base compose is core only (dnsmasq, drasl, minecraft, mc-backup, caddy
auth+packwiz). docker-compose.static.yml and docker-compose.mirror.yml layer
on the landing site and the air-gap mirror independently. build-stack.sh --up
[core|static|mirror|full] selects the override files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Base Caddyfile now imports conf.d/*.caddy. Snippets are mounted selectively
so the stack runs with or without the static site / air-gap mirror:
00-core auth + packwiz (always)
10-static apex landing + launcher downloads
20-mirror air-gap upstream hosts (tls internal)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mounting ./mirror/launcher at /srv/www/launcher failed: /srv/www is a
read-only mount, so the nested mountpoint can't be created. Mount it at
/srv/launcher and route /launcher/* via handle_path instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MSVC and MinGW Setup .exe both matched 'setup.*.exe'; tightened the
include pattern to 'msvc-setup.*.exe' so exactly one candidate resolves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>