# Ulicraft Server — Plan Overview Self-hosted modded Minecraft LAN-party stack, designed to run **fully offline** (air-gapped) during the party while remaining a persistent homelab service. ## Single source of configuration Everything is driven by two env vars in `.env`: ``` BASE_DOMAIN=ulicraft.local # all services are subdomains of this HOST_LAN_IP=192.168.x.x # the Docker host's LAN IP (dnsmasq target) ``` > `.local` collides with mDNS/Bonjour (RFC 6762). macOS + Linux Avahi may resolve > `*.local` via multicast and bypass dnsmasq. Accepted risk for now; alternatives > if it bites: `.lan`, `home.arpa`. Flagged, user chose `.local`. ## Subdomain / ingress map ``` ulicraft.local → caddy → landing page (launcher downloads + guide) auth.ulicraft.local → caddy → drasl:25585 (auth web UI + Yggdrasil API) packwiz.ulicraft.local → caddy → /srv/pack (pack metadata + mod-jar mirror) assets.ulicraft.local → caddy → /srv/assets (MC asset-object mirror; HTTPS forced — see 10) mc.ulicraft.local:25565 → minecraft (NOT http; raw MC protocol) ``` Caddy is the only HTTP ingress (plain `:80` for now, TLS/step-ca later). drasl's host port is removed — internal only, reached through Caddy. ### Internal resolution trick Containers use the Docker resolver, NOT dnsmasq. Caddy gets network aliases so the stack resolves its own subdomains internally: ```yaml caddy: networks: mcnet: aliases: [ "auth.${BASE_DOMAIN}", "packwiz.${BASE_DOMAIN}" ] ``` Result: `http://auth.ulicraft.local/authlib-injector` and `http://packwiz.ulicraft.local/pack.toml` resolve identically inside and outside the stack. dnsmasq exists **only for guest laptops**. ## Online vs offline split ``` PRE-PARTY (internet, run once): 1. curate packwiz pack → see 04-packwiz.md 2. mirror mod jars + rewrite URLs → tooling/mirror-mods.sh (08-tooling.md) 3. pre-bake server volume → TYPE=NEOFORGE install (05-minecraft.md) 4. download launcher releases → tooling/fetch-launcher.sh (06-launcher.md) 5. capture+mirror client air-gap → tooling/mirror-airgap.sh (11-full-airgap-mirror.md) 6. build landing page → cd landing && pnpm run build → www/ (09-landing.md) 7. render configs from templates → tooling/render-config.sh (08-tooling.md) PARTY (air-gapped): dnsmasq : *.ulicraft.local → HOST_LAN_IP caddy : local CDN + landing page drasl : password login (NO Keycloak/OIDC for now) minecraft: TYPE=CUSTOM, no Mojang re-resolution guests : install Fjord launcher, import pack, add authlib account → auth subdomain ``` ## Why each hard call was made - **No OIDC/Keycloak (for now)** — drasl password login. Keycloak left out of this stack's scope. Re-add later if desired. - **itzg can't air-gap-boot in NEOFORGE mode** (re-resolves Mojang metadata every start, issue #2340). Pre-bake then switch to `TYPE=CUSTOM`. - **packwiz only serves metadata** — `.pw.toml` points at Modrinth CDN. Offline needs jars mirrored onto Caddy + URL rewrite. - **Fjord launcher** has authlib-injector built in → no manual JVM agent on clients. ## Target folder layout ``` . ├── docker-compose.yml ├── .env / .env.example # BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD ├── caddy/Caddyfile # {$BASE_DOMAIN} vhosts + landing page ├── dnsmasq/dnsmasq.conf.tmpl # wildcard template ├── drasl/config/ │ ├── config.toml.tmpl # envsubst source │ └── config.toml # generated (gitignored) ├── pack/ # packwiz source of truth ├── mirror/ # vendored mod jars + launcher releases (gitignored) ├── runtime/ # authlib-injector.jar (legacy; may be unused w/ Fjord) ├── tooling/ │ ├── render-config.sh # envsubst templates → real configs, halt if unset │ ├── mirror-mods.sh # vendor mod jars + rewrite .pw.toml URLs │ ├── mirror-assets.sh # mirror MC asset objects for offline clients (10) │ └── fetch-launcher.sh # download all Fjord release assets ├── landing/ # Astro+TS landing source (build → www/) │ ├── astro.config.mjs # outDir ../www │ ├── public/logo.png # stone 3D logo │ └── src/{data/site.ts,pages/index.astro} ├── www/ # caddy landing page — GENERATED by landing build (gitignored) ├── backups/ └── plan/ # these files ``` ## Service plan files - `01-dnsmasq.md` — LAN wildcard DNS - `02-caddy.md` — ingress + local CDN + landing page - `03-drasl.md` — auth (password login) - `04-packwiz.md` — modpack source + mod mirror - `05-minecraft.md` — itzg NeoForge server + offline launch - `06-launcher.md` — Fjord launcher fetch + distribution - `07-mc-backup.md` — world backups - `08-tooling.md` — render/mirror/fetch scripts - `09-landing.md` — Astro+TS guest onboarding page → www/ - `10-assets-mirror.md` — lighter alternative: objects-only Fjord Assets Server override - `11-full-airgap-mirror.md` — CHOSEN: DNS+TLS transparent mirror for full client air-gap - `12-build-order.md` — commit-per-task build sequence - `13-dns-and-run-modes.md` — domain (.lan), online/airgap modes, party DNS, mDNS option ## Boot/dependency order `dnsmasq` → `caddy` (aliases) → `drasl` → `minecraft` (depends on drasl + packwiz served by caddy) → `mc-backup`.