Capture what this session learned the hard way, in plan/03-drasl.md and CLAUDE.md: - Correct admin key is DefaultAdmins (not DefaultAdminUsernames); wrong/unknown keys are silently ignored by drasl (logged as "unknown config option"). - CORSAllowOrigins must be top-level, before any [Section], or it's ignored. - First-admin bootstrap under invite mode is a chicken-egg → temp-flip RequireInvite=false, register, revert. - NMSR needs the auth. host-gateway pin + https mojank endpoints or avatars render the default skin (cross-ref plan/19-routes.md). - Fix the stale example config and mark the admin-bootstrap task done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
184 lines
9.8 KiB
Markdown
184 lines
9.8 KiB
Markdown
# Ulicraft Server — Project Context
|
||
|
||
> Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
|
||
> (Drasl), a distribution-fed modpack, avatar rendering, a guest landing page, and uptime
|
||
> monitoring. Public, internet-present deployment behind the host's nginx + TLS.
|
||
|
||
**`plan/` is the source of truth for the design.** Start at `plan/00-overview.md`.
|
||
This file is a fast orientation + the durable decisions/gotchas; when it disagrees
|
||
with `plan/` or the code, those win.
|
||
|
||
## Goals & Constraints
|
||
|
||
- **Players**: 4–10 friends
|
||
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
|
||
- **Pack approach**: Manually curated (~50–100 mods), NOT a forked existing pack
|
||
- **Minecraft version**: 1.21.1, **NeoForge** (not classic Forge)
|
||
- **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC
|
||
- **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host
|
||
- **Persistence horizon**: long-term service, not disposable
|
||
|
||
## Architecture
|
||
|
||
One unified `docker-compose.yml`. The host's own nginx terminates TLS (Let's
|
||
Encrypt) and reverse-proxies every vhost to caddy (published localhost-only) by
|
||
Host header. caddy is the internal router; containers reach the stack's own names
|
||
via caddy's `mcnet` aliases (`auth.`, `pack.`).
|
||
|
||
```
|
||
Internet ── host nginx (TLS, LE certs, HSTS) ──► caddy (127.0.0.1:8880)
|
||
│ routes by Host:
|
||
${BASE_DOMAIN} ─► /srv/www landing + /launcher/ downloads
|
||
auth.${BASE_DOMAIN} ─► drasl (Yggdrasil API + web UI)
|
||
avatar.${BASE_DOMAIN} ─► nmsr (skin/avatar renderer, Drasl-backed)
|
||
status.${BASE_DOMAIN} ─► uptime-kuma (status page + monitors)
|
||
distribution.${BASE} ─► static site from ANOTHER repo (DISTRIBUTION_WEB_ROOT)
|
||
|
||
minecraft :25565 (+ 24454/udp Simple Voice Chat)
|
||
└ ONLINE_MODE=false, -javaagent authlib-injector → http://auth.${BASE_DOMAIN}/authlib-injector
|
||
└ mods from ./server-mods volume (/mods, REMOVE_OLD_MODS) — see plan/04-mods.md
|
||
mc-backup ── RCON → minecraft (6h interval, prune 14d, world-only)
|
||
```
|
||
|
||
Bring-up: render configs → sync server mods → build landing → fetch launcher → `docker compose up -d --build`.
|
||
See `plan/12-build-order.md` and `plan/14-deploy.md`. Deploy to prod host `cochi`
|
||
via the `deploy` skill.
|
||
|
||
## Key Decisions
|
||
|
||
- **NeoForge over Forge** — the 1.21.x kitchen-sink ecosystem is overwhelmingly
|
||
NeoForge; the Forge team migrated. `itzg/minecraft-server` via `TYPE=NEOFORGE`.
|
||
- **Drasl over Ely.by / vanilla offline** — self-hosted Go service, Yggdrasil
|
||
drop-in, skins + capes, actively maintained. Run with **password login**
|
||
(no OIDC/Keycloak in this stack).
|
||
- **Manual curation** (user choice, flagged expensive) — if it gets painful,
|
||
fork-and-trim Leaking Kitchen Sink is still on the table (closest vibe match).
|
||
- **itzg/minecraft-server** base image — de-facto standard, auto-installs NeoForge.
|
||
- **Distribution-fed mod volume (replaced packwiz)** — the modset's source of
|
||
truth is the HeliosLauncher distribution repo (`$DISTRIBUTION_WEB_ROOT`).
|
||
Clients install the full set via `distribution.json`. The SERVER loads a
|
||
filtered subset (`both`/`server`) from a mounted `./server-mods` volume
|
||
(`REMOVE_OLD_MODS=TRUE`). See `plan/04-mods.md`.
|
||
|
||
## Critical Gotchas
|
||
|
||
### Minecraft 1.21+ secure profile
|
||
- Server: `ENFORCE_SECURE_PROFILE=FALSE` (`enforce-secure-profile=false`)
|
||
- Drasl: `SignPublicKeys = false`
|
||
- Linked. Drasl docs: *"Mixed authentication does not work with
|
||
`SignPublicKeys = true` on Minecraft 1.21+."*
|
||
|
||
### authlib-injector JVM agent
|
||
- Syntax: `-javaagent:/extras/authlib-injector.jar=<yggdrasil-api-url>`
|
||
- URL after `=` is the **API root** = `<BaseURL>/authlib-injector`. For this
|
||
stack: `http://auth.${BASE_DOMAIN}/authlib-injector` (internal, over mcnet).
|
||
- **Must match between server and client** (clients use the public
|
||
`https://auth.${BASE_DOMAIN}/authlib-injector`), else session validation fails
|
||
silently with "Invalid session".
|
||
- Releases: https://github.com/yushijinhun/authlib-injector — jar lives in
|
||
`runtime/` (mounted at `/extras/authlib-injector.jar`).
|
||
|
||
### NeoForge version pinning
|
||
Mods are picky about exact minor versions. Compose pins `NEOFORGE_VERSION:
|
||
"21.1.233"` — **must match the distribution's NeoForge** (`distribution.json`),
|
||
since mods are built against it (e.g. ferritecore≥218, farmersdelight≥219).
|
||
**Verify against https://projects.neoforged.net/neoforged/neoforge before
|
||
deploying.** Never `"latest"` for a stable server.
|
||
|
||
### Mod source + server filtering
|
||
The full modset lives in the distribution repo (`$DISTRIBUTION_WEB_ROOT/servers/
|
||
ulicraft-1.21.1/forgemods/required/*.jar`), managed by Nebula. Clients get
|
||
everything via `distribution.json`. The server runs a **filtered subset**:
|
||
`tooling/classify-mods.py` reads each jar's `neoforge.mods.toml` with tomllib and
|
||
seeds `mods-sides.json` (`client|server|both`, default `both`), then
|
||
`tooling/sync-server-mods.sh` copies the `both`/`server` jars into `./server-mods`
|
||
(mounted at `/mods`). Hard client-only mods (sodium/iris) classify as `client`
|
||
automatically; cosmetic-client mods that declare `BOTH` stay `both` until you
|
||
hand-edit `mods-sides.json` to `client`. No Modrinth/packwiz, no network at
|
||
classify/sync time. See `plan/04-mods.md`.
|
||
|
||
### Memory sizing
|
||
~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
|
||
`USE_AIKAR_FLAGS: TRUE`.
|
||
|
||
## Config (.env)
|
||
|
||
`BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT`/`CADDY_HTTP_BIND`,
|
||
`DISTRIBUTION_WEB_ROOT` (source of the modset + caddy `distribution.` mount), the
|
||
Let's Encrypt block (`LE_EMAIL`, `LE_SUBDOMAINS`, `OVH_*`). Rendered configs
|
||
(drasl, nmsr) come from `*.tmpl` via `tooling/render-config.sh` (substitutes
|
||
`${BASE_DOMAIN}` only). Server mods are synced (not rendered) by
|
||
`tooling/sync-server-mods.sh`.
|
||
|
||
## Client Distribution (guests)
|
||
|
||
Launcher: **FjordLauncherUnlocked** (Prism fork, first-class authlib-injector),
|
||
mirrored at apex `/launcher/`. Per guest:
|
||
1. Download from `https://${BASE_DOMAIN}` → `/launcher/`.
|
||
2. Add an authlib-injector account, URL
|
||
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
|
||
`https://auth.${BASE_DOMAIN}`).
|
||
3. The launcher installs the modpack from the distribution (`distribution.json`).
|
||
(NOTE: packwiz/`pack.` removed — the landing join step still references a
|
||
packwiz URL and needs reworking around the launcher/distribution flow.)
|
||
4. Connect to `${BASE_DOMAIN}` (`:25565`).
|
||
|
||
## Open / Pending Work
|
||
|
||
### Roadmap — mod migration follow-ups (post packwiz→04-mods)
|
||
- [ ] **Curate `mods-sides.json` cosmetic-client mods.** tomllib auto-flagged 11
|
||
hard client-only mods; the other 65 default `both`, including client-cosmetic
|
||
ones (BetterF3, entityculling, MouseTweaks, ponderjs, fancymenu, drippy, melody,
|
||
welcomescreen, Searchables, JustEnoughResources, TravelersTitles, yeetus…) that
|
||
load harmlessly on the server but waste RAM. Hand-edit them to `client` so
|
||
`sync-server-mods.sh` drops them from `./server-mods`. (Also review `appleskin`,
|
||
flagged `client` — flip to `both` if server-side food data wanted.)
|
||
### Landing rework + mod list (PLANNED — see `plan/18-landing-rework.md`)
|
||
Full design settled across WS1–WS6; all tasks unchecked, ready to execute.
|
||
- [ ] **WS1 — Join flow off packwiz.** Homepage = UlicraftLauncher, 3 steps
|
||
(register → download launcher → join); drop `packwizUrl` + packwiz step.
|
||
Per-OS downloads from a synced `launcher-manifest.json` (contract:
|
||
`landing/src/data/launcher-manifest.schema.json`, produced by the
|
||
`custom-launcher` repo). Fjord moves to its own `/fjord` page.
|
||
- [ ] **WS2/3 — Player rosters.** Online (mc-status, live) + all-registered (new
|
||
`mc-status /api/mcstatus/players`, admin-login → Drasl `GET /players`, ~60s
|
||
cache). Shared avatar tile, `AVATAR_MODE` env (default `headiso`). Admin creds
|
||
→ mc-status only.
|
||
- [ ] **WS4 — Account page** (`/account`): skin CRUD via browser-direct Drasl
|
||
(reuses `register.astro` skin JS). Skins only, in-memory token, `players[0]`.
|
||
- [ ] **WS5 — Footer status link** to `status.${BASE_DOMAIN}` (nav unchanged).
|
||
|
||
- [ ] **WS6 — Mod shortlist + list component (`plan/17-mod-shortlist.md`).**
|
||
Light gap doc: current 76-jar pack is vanilla+ QoL/perf; **empty** on tech &
|
||
magic, thin worldgen, no map; orphan deps `ponderjs`/`Necronomicon`. Landing
|
||
mod-list component = auto-generated `mods.json` + extracted logos from the
|
||
distribution forgemods (`tooling/build-modlist.py`), flat alphabetical, pretty
|
||
names, no categories/links; feeds the "50+" stat tile.
|
||
- [ ] Server-side perf mods (C2ME, etc.).
|
||
- [ ] **Verify NeoForge version** against current stable before first deploy.
|
||
- [x] **Bootstrap Drasl admin** account (`admin`, 2026-06-10). Key is
|
||
`DefaultAdmins` (not `DefaultAdminUsernames`); first admin needs an invite-flip.
|
||
See `plan/03-drasl.md` gotchas.
|
||
|
||
## Reference URLs
|
||
|
||
- Drasl: https://github.com/unmojang/drasl — config docs:
|
||
https://github.com/unmojang/drasl/blob/master/doc/configuration.md
|
||
- authlib-injector: https://github.com/yushijinhun/authlib-injector
|
||
- itzg/minecraft-server: https://github.com/itzg/docker-minecraft-server
|
||
(NeoForge: …/docs/types-and-platforms/server-types/forge.md)
|
||
- itzg/mc-backup: https://github.com/itzg/docker-mc-backup
|
||
- NeoForge versions: https://projects.neoforged.net/neoforged/neoforge
|
||
- NMSR: https://github.com/NickAcPT/nmsr-rs
|
||
- Uptime Kuma: https://github.com/louislam/uptime-kuma
|
||
- FjordLauncherUnlocked: https://github.com/hero-persson/FjordLauncherUnlocked
|
||
- Reference packs (inspiration, NOT forking): ATM10, Leaking Kitchen Sink.
|
||
|
||
## Communication Preferences (carry-over)
|
||
|
||
- Concise and direct; diagnose + resolve without demanding repro steps.
|
||
- Prefer iterative single-change updates over large rewrites.
|
||
- Cautious scripts that halt on ambiguity rather than proceeding silently.
|
||
- Avoid over-engineering when a simple answer suffices.
|
||
- Spanish or English both fine.
|