5.5 KiB
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)
.localcollides with mDNS/Bonjour (RFC 6762). macOS + Linux Avahi may resolve*.localvia 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:
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.tomlpoints 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 DNS02-caddy.md— ingress + local CDN + landing page03-drasl.md— auth (password login)04-packwiz.md— modpack source + mod mirror05-minecraft.md— itzg NeoForge server + offline launch06-launcher.md— Fjord launcher fetch + distribution07-mc-backup.md— world backups08-tooling.md— render/mirror/fetch scripts09-landing.md— Astro+TS guest onboarding page → www/10-assets-mirror.md— lighter alternative: objects-only Fjord Assets Server override11-full-airgap-mirror.md— CHOSEN: DNS+TLS transparent mirror for full client air-gap12-build-order.md— commit-per-task build sequence13-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.