Commit Graph

48 Commits

Author SHA1 Message Date
843347ad00 feat(tls): Let's Encrypt via OVH DNS-01 + host nginx vhost
tooling/issue-letsencrypt.sh: manual script, issues one cert per name
(apex + LE_SUBDOMAINS) with acme.sh + OVH DNS-01 (no inbound ports),
installs to certs/<name>/ and reloads nginx. nginx/ulicraft.conf.tmpl:
TLS vhosts for apex/pack (static) + auth (proxy to drasl). OVH creds and
LE_EMAIL in .env.example; certs/ gitignored. plan/15-letsencrypt.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:35:07 +02:00
4c874e20ed refactor(ingress): extract caddy to its own compose file; add nginx path
Caddy is no longer in the base stack. Two ingress paths now:
- docker-compose.caddy.yml — LAN/air-gap; build-stack.sh adds it for all
  up modes + the prep pre-bake. Holds the mcnet auth./pack. aliases.
- docker-compose.nginx.yml — production behind the host's nginx: publishes
  drasl on 127.0.0.1:25585, points minecraft at the host (extra_hosts) over
  HTTPS for pack + authlib.

Base loses the caddy service and minecraft's caddy depends_on (re-added by
caddy.yml). Blessing Skin override now requires caddy.yml (it only swaps
caddy's core conf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:35:07 +02:00
67718d8e05 docs(deploy): cochi redeploy guide + deploy skill
plan/14-deploy.md: git pull main + hard restart (compose down ->
build-stack.sh --up online), drasl/online, verify + rollback.
.claude/skills/deploy: skill to run it (pre-check, pull, hard restart,
verify) with prod guardrails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:22:37 +02:00
6e91749f59 refactor(pack): template CustomSkinLoader + unify pack rendering
Add tooling/render-pack.sh: renders all pack/**/*.tmpl with ${BASE_DOMAIN}
(no dnsmasq HOST_LAN_IP coupling) and rebuilds the packwiz index. Both
render-config.sh and add-custom-mod.sh now delegate to it.

CustomSkinLoader.json's skin API roots are no longer frozen — it becomes
CustomSkinLoader.json.tmpl, rendered like the mod metadata. .packwizignore
broadened to *.tmpl; rendered .json gitignored as build output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:42:32 +02:00
853405afe7 chore(skins): point CustomSkinLoader at auth.ulicraft.net
Update apiRoot/sessionRoot from the old .lan domain to .net.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:38:30 +02:00
f7858d4ec4 refactor(pack): template custom mod metadata so BASE_DOMAIN isn't frozen
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>
2026-06-07 22:07:35 +02:00
39be74d028 fix(pack): regenerate custom mod URLs for current BASE_DOMAIN
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>
2026-06-07 21:52:00 +02:00
c40cbe7594 refactor(pack): host custom jars from ./custom outside the pack root
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>
2026-06-07 21:22:14 +02:00
58f0312018 feat(pack): add QoL/JEI mod set as packwiz external files
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>
2026-06-07 20:28:51 +02:00
8c3d85f691 refactor(ingress): rename packwiz.* subdomain to pack.*
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>
2026-06-07 20:28:07 +02:00
2405eee57d feat(tooling): add-custom-mod.sh folder mode
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>
2026-06-07 18:51:56 +02:00
e1ccdda961 feat(tooling): add-custom-mod.sh — host local jars as packwiz external files
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>
2026-06-07 18:48:55 +02:00
5172316919 feat(auth): Blessing Skin variant as Drasl alternative
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>
2026-06-07 18:48:54 +02:00
c279f35e14 feat(dns): 'hosts' output + /etc/hosts simulation docs
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>
2026-05-24 14:23:14 +02:00
d8aefe95c4 feat(tooling): mdns-host-setup.sh — pin host avahi to the LAN NIC
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>
2026-05-24 14:11:41 +02:00
3c2ce72c2a docs(plan): add 13-dns-and-run-modes; mark dnsmasq optional
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 06:06:33 +02:00
64f11c8120 feat(build-stack): mDNS preflight warnings (host avahi, dbus, interface count)
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>
2026-05-24 06:05:44 +02:00
c7151b7333 fix(mdns): publish via host avahi over D-Bus (not a second daemon)
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>
2026-05-24 06:03:26 +02:00
de75a2ca1d docs: update README for .lan, online/airgap modes, party DNS + mDNS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 05:57:26 +02:00
8eeda1265c feat(mdns): optional Avahi .local responder (ENABLE_MDNS)
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>
2026-05-24 05:56:54 +02:00
f14578dc65 feat(dns): party-DNS records generator + online/airgap modes
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>
2026-05-24 05:56:54 +02:00
390f8c78af feat(caddy): serve Caddy root CA at /ca.crt for guest trust
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>
2026-05-24 05:32:31 +02:00
b277fc6268 feat(build-stack): fetch authlib-injector.jar during prep
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>
2026-05-24 05:20:02 +02:00
4ddf36cefd docs: add README (stack summary + launch instructions)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 05:10:44 +02:00
1aed12bf00 fix(compose): scope upstream spoof aliases to mirror mode
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>
2026-05-24 05:10:13 +02:00
2d7a7554bd fix(drasl): ApplicationOwner is a string, not a table (config parse crash)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 05:10:13 +02:00
df8561c798 feat(dnsmasq): SRV record so clients connect to mc.<domain> without a port
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>
2026-05-24 05:08:07 +02:00
d6e7504d6e feat(compose): optional static/mirror via override files; build-stack up modes
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>
2026-05-24 05:07:20 +02:00
3bf714a224 refactor(caddy): split vhosts into conf.d snippets (core/static/mirror)
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>
2026-05-24 05:07:20 +02:00
78e92a65be fix(caddy): serve launcher from sibling mount, not nested under /srv/www
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>
2026-05-24 05:04:59 +02:00
eede0102c6 fix(tooling): fetch-launcher Windows stable symlink picks MSVC Setup x64
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>
2026-05-24 05:02:02 +02:00
f513f60766 feat(caddy): add tls internal vhosts for upstream mirror hosts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:47:38 +02:00
a6c4d3be8c feat(dnsmasq): add air-gap spoof records for upstream hosts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:47:10 +02:00
33eb5c60e6 feat(tooling): add mirror-airgap.sh (deterministic mirror + capture notice)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:47:02 +02:00
a6a43a17ac feat(tooling): add fetch-launcher.sh (all platforms + stable symlinks)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:44:27 +02:00
8f6d846858 refactor(compose): minecraft via PACKWIZ_URL, authlib->auth., runtime mount
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:42:55 +02:00
31eadcfb32 refactor(runtime): rename extras/ to runtime/, drop obsolete modrinth list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:42:19 +02:00
5f7a197c56 feat(tooling): add mirror-mods.sh (vendor jars + rewrite .pw.toml URLs)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:40:56 +02:00
7f10cb24c9 feat(pack): packwiz init (NeoForge 1.21.1) + curation README
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:40:04 +02:00
0446fc68ac refactor(compose): drasl internal-only behind caddy
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:38:53 +02:00
e61286d2b3 feat(drasl): add config.toml.tmpl (password login, SignPublicKeys=false)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:38:41 +02:00
e351736e2e feat(compose): add caddy service + network aliases
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:38:04 +02:00
6d09f659db feat(caddy): add Caddyfile (apex + auth + packwiz vhosts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:34:33 +02:00
c717890012 feat(compose): add dnsmasq service
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:11:45 +02:00
b24814a214 feat(dnsmasq): add dnsmasq.conf.tmpl (wildcard *.BASE_DOMAIN)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:11:04 +02:00
1300a02070 feat(tooling): add render-config.sh (envsubst, halt on unset var)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:07:54 +02:00
26cbaa1bdf feat(env): add BASE_DOMAIN + HOST_LAN_IP to .env.example
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:06:56 +02:00
c09da15bd0 initial commit 2026-05-24 04:03:42 +02:00