Files
ulicraft-server-v1/plan/00-overview.md
2026-05-24 06:06:33 +02:00

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)

.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:

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

dnsmasqcaddy (aliases) → draslminecraft (depends on drasl + packwiz served by caddy) → mc-backup.