Files
ulicraft-server-v1/CLAUDE.md
Oier Bravo Urtasun fa8cede6ae docs(roadmap): track post-migration mod follow-ups
Two follow-ups from the packwiz→04-mods migration: curate cosmetic-client
entries in mods-sides.json, and rework the landing join flow off the dead
packwiz pack.toml URL onto the HeliosLauncher/distribution.json flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:03:07 +02:00

9.0 KiB
Raw Blame History

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: 410 friends
  • Modpack vibe: Kitchen-sink (tech + magic + exploration + QoL)
  • Pack approach: Manually curated (~50100 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

~50100 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.)

  • Rework the landing join flow off packwiz. pack. vhost is gone; site.ts packwizUrl is a dead-URL placeholder (kept w/ TODO so the build passes) and i18n/ui.ts join steps (all 3 langs) still tell guests to import pack.toml. Redesign step 3 around the HeliosLauncher/distribution.json install flow. See plan/04-mods.md + site.ts TODO.

  • Mod shortlist for the kitchen-sink pack: Performance (Embeddium, FerriteCore, ModernFix), Tech (Mekanism, Create, IE, AE2), Magic (Ars Nouveau, Botania, Iron's Spells), Storage (Sophisticated, Functional), Exploration (YUNG's, Repurposed Structures), QoL (JEI, Jade, IPN, AppleSkin), World gen (Tectonic, Terralith — verify NeoForge 1.21.1), Compat (Polymorph), Map (Xaero), Voice (Simple Voice Chat — 24454/udp already in compose).

  • Server-side perf mods (C2ME, etc.).

  • Verify NeoForge version against current stable before first deploy.

  • Bootstrap Drasl admin account.

Reference URLs

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.