initial commit

This commit is contained in:
2026-05-24 04:03:42 +02:00
commit c09da15bd0
28 changed files with 5801 additions and 0 deletions

122
plan/00-overview.md Normal file
View File

@@ -0,0 +1,122 @@
# 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
## Boot/dependency order
`dnsmasq``caddy` (aliases) → `drasl``minecraft` (depends on drasl + packwiz served by caddy) → `mc-backup`.