# 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.`). ``` 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=` - URL after `=` is the **API root** = `/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`). (packwiz and the `pack.` service/subdomain have been fully removed.) 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.json` (shape `productName/version/files[]`, the build output of the `custom-launcher` repo; `launcher.example.json` committed as the documented fallback). 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.