docs(claude): rewrite project context for unified stack

Drop stale Keycloak/OIDC, NetBird, AdGuard/.home.local, airgap, and
Prism-specific content. Reflect the current reality: single docker-compose.yml
(drasl, minecraft, mc-backup, caddy, nmsr, uptime-kuma), Drasl password-login
auth (no OIDC), public nginx+LE -> caddy ingress, DNS handled outside the repo,
FjordLauncher for guests. Point to plan/ as the source of truth; keep the
still-valid gotchas (secure-profile, authlib-injector, NeoForge pinning, mod
source) and carry-over comms prefs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 02:16:42 +02:00
parent 67d1f82e6a
commit 1709bcd340

343
CLAUDE.md
View File

@@ -1,280 +1,143 @@
# Minecraft LAN Party Server — Project Context # Ulicraft Server — Project Context
> Self-hosted modded Minecraft server for a LAN party (~8 players), with > Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
> persistent auth/skin infrastructure designed to outlive the LAN weekend > (Drasl), a packwiz modpack, avatar rendering, a guest landing page, and uptime
> and integrate with the existing homelab. > 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 ## Goals & Constraints
- **Players**: 410 friends, LAN-based - **Players**: 410 friends
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL) - **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
- **Pack approach**: Manually curated (~50100 mods), NOT a forked existing pack - **Pack approach**: Manually curated (~50100 mods), NOT a forked existing pack
- **Minecraft version**: 1.21.1 - **Minecraft version**: 1.21.1, **NeoForge** (not classic Forge)
- **Mod loader**: NeoForge (NOT classic Forge — see Decisions below) - **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC
- **Network**: LAN party context, but stack is persistent (lives past the weekend) - **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host
- **Auth**: Self-hosted Yggdrasil-compatible (Drasl) + Keycloak OIDC - **Persistence horizon**: long-term service, not disposable
- **Skins**: Handled by Drasl
- **Persistence horizon**: Long-term homelab service, not disposable
## Existing Homelab Context (relevant to this project)
- **Keycloak 26.0.7** running in Docker Compose (PostgreSQL 17, production mode,
bind-mounted secrets, xforwarded proxy headers). This is the IdP for everything.
- **NetBird** as the proxy/networking layer for remote access (NOT used for the
LAN party itself — see Decisions).
- **AdGuard Home** as recursive DNS resolver — used here for `*.home.local` LAN records.
- **Multi-VPS topology**: this Minecraft project likely runs on a local box or
beefier homelab node, not a VPS (RAM/CPU requirements).
- Operator timezone: Europe/Madrid.
## Key Decisions (with reasoning)
### NeoForge over classic Forge
Despite the user originally saying "Forge", the 1.21.x kitchen-sink ecosystem
(ATM10, Leaking Kitchen Sink, etc.) is overwhelmingly NeoForge in 2025/2026.
The original Forge team essentially migrated to NeoForge. `itzg/minecraft-server`
supports both via `TYPE=NEOFORGE`. Going with NeoForge 1.21.1.
### Drasl over Ely.by or vanilla offline mode
- **Ely.by**: not self-hosted, Russian-hosted, no control over identity layer.
- **Vanilla offline + no auth server**: works for one weekend but skins break,
no persistent identity, doesn't match homelab philosophy.
- **Drasl**: self-hosted Go service, Yggdrasil-compatible, drop-in Mojang
replacement, supports skins + capes, **has first-class OIDC support with
PKCE** (perfect for Keycloak), actively maintained (current version 3.4.2+).
GPLv3 licensed.
### LAN (not NetBird) for the party itself
NetBird is the homelab's remote access layer, but adding a mesh VPN on top
of a LAN party introduces complexity for guests with no upside. Drasl and
Minecraft live on the LAN; clients reach them via `drasl.home.local` resolved
through AdGuard.
### Manual curation (user choice — flagged as expensive)
User insisted on manual curation despite my pushback that forking ATM10 or
Leaking Kitchen Sink would save weeks of crash-log triage. **Note for future
sessions**: if curation gets painful, the fork-and-trim option is still on
the table — Leaking Kitchen Sink is the closest match to the requested vibe.
### itzg/minecraft-server as base image
The de facto standard. Handles NeoForge installation automatically via
`TYPE=NEOFORGE` + `VERSION` + `NEOFORGE_VERSION`. Supports Modrinth and
CurseForge mod auto-download via `MODRINTH_PROJECTS` and `CURSEFORGE_FILES`.
## Architecture ## Architecture
``` One unified `docker-compose.yml`. The host's own nginx terminates TLS (Let's
LAN segment 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
├── AdGuard Home → resolves drasl.home.local, keycloak.home.local via caddy's `mcnet` aliases (`auth.`, `pack.`).
│ to the Docker host's LAN IP
├── Keycloak (existing, separate compose)
│ realm: homelab
│ client: drasl (confidential, PKCE S256)
└── Minecraft host (this project's compose)
├── drasl :25585 (Yggdrasil API + web UI)
│ └── OIDC → Keycloak
├── minecraft :25565 (server)
│ :24454/udp (Simple Voice Chat, optional)
│ └── -javaagent:authlib-injector.jar=http://drasl.home.local:25585/authlib-injector
│ └── ONLINE_MODE=false (required for authlib-injector)
└── mc-backup (itzg/mc-backup, 6h interval)
└── RCON → minecraft
Clients (LAN party guests): ```
Prism Launcher → authlib-injector account pointing at Drasl Internet ── host nginx (TLS, LE certs, HSTS) ──► caddy (127.0.0.1:8880)
→ joins minecraft:25565 │ routes by Host:
${BASE_DOMAIN} ─► /srv/www landing + /launcher/ downloads
auth.${BASE_DOMAIN} ─► drasl (Yggdrasil API + web UI)
pack.${BASE_DOMAIN} ─► packwiz files + /custom/ jars
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 via PACKWIZ_URL → http://pack.${BASE_DOMAIN}/pack.toml
mc-backup ── RCON → minecraft (6h interval, prune 14d, world-only)
``` ```
## Auth Flow (end-to-end) Bring-up: render configs → 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.
1. Guest opens `http://drasl.home.local:25585` in browser. ## Key Decisions
2. Clicks "Register with Keycloak" → redirected to Keycloak login.
3. Logs in with homelab Keycloak credentials. - **NeoForge over Forge** — the 1.21.x kitchen-sink ecosystem is overwhelmingly
4. Returned to Drasl, picks a player name, uploads a skin. NeoForge; the Forge team migrated. `itzg/minecraft-server` via `TYPE=NEOFORGE`.
5. Drasl shows them a "Minecraft Token" on their profile page. - **Drasl over Ely.by / vanilla offline** — self-hosted Go service, Yggdrasil
6. In Prism Launcher: Settings → Accounts → Add authlib-injector account drop-in, skins + capes, actively maintained. Run with **password login**
with URL `http://drasl.home.local:25585/authlib-injector` and the (no OIDC/Keycloak in this stack).
Minecraft Token as password. - **Manual curation** (user choice, flagged expensive) — if it gets painful,
7. Joins Minecraft server. Server validates session against Drasl (via fork-and-trim Leaking Kitchen Sink is still on the table (closest vibe match).
authlib-injector JVM agent), Drasl confirms, player enters with their skin. - **itzg/minecraft-server** base image — de-facto standard, auto-installs NeoForge.
- **packwiz** for the modpack — `PACKWIZ_URL` drives server mod install; clients
import the same `pack.toml`. Jars come from the upstream CDN per `.pw.toml`.
## Critical Gotchas ## Critical Gotchas
### Minecraft 1.21+ secure profile incompatibility ### Minecraft 1.21+ secure profile
- Server: must set `enforce-secure-profile=false` (compose: `ENFORCE_SECURE_PROFILE=FALSE`) - Server: `ENFORCE_SECURE_PROFILE=FALSE` (`enforce-secure-profile=false`)
- Drasl: must set `SignPublicKeys = false` - Drasl: `SignPublicKeys = false`
- These are linked. The Drasl docs explicitly warn: *"Mixed authentication does - Linked. Drasl docs: *"Mixed authentication does not work with
not work with `SignPublicKeys = true` on Minecraft 1.21+."* `SignPublicKeys = true` on Minecraft 1.21+."*
### authlib-injector JVM agent ### authlib-injector JVM agent
- Syntax: `-javaagent:/path/to/authlib-injector.jar=<yggdrasil-api-url>` - Syntax: `-javaagent:/extras/authlib-injector.jar=<yggdrasil-api-url>`
- The URL after `=` is the **API root**, which for Drasl is - URL after `=` is the **API root** = `<BaseURL>/authlib-injector`. For this
`<BaseURL>/authlib-injector`. stack: `http://auth.${BASE_DOMAIN}/authlib-injector` (internal, over mcnet).
- **Must match between server and client.** If server uses - **Must match between server and client** (clients use the public
`http://drasl.home.local:25585/authlib-injector` and client uses anything `https://auth.${BASE_DOMAIN}/authlib-injector`), else session validation fails
else (e.g. an IP), session validation fails silently with "Invalid session". silently with "Invalid session".
- Download from: https://github.com/yushijinhun/authlib-injector/releases - Releases: https://github.com/yushijinhun/authlib-injector — jar lives in
- Mount into the itzg container at `/extras/authlib-injector.jar`. `runtime/` (mounted at `/extras/authlib-injector.jar`).
### Keycloak reachability on LAN day
If Keycloak is only reachable via NetBird or only from the public internet,
**Drasl OIDC registration breaks on LAN-only day**. Options:
1. Expose Keycloak on the LAN (simplest)
2. Pre-register all guests before the party
3. Temporarily disable OIDC and allow password registration during the LAN
(`AllowPasswordLogin = true`, comment out the OIDC block)
### HTTP vs HTTPS for OIDC
Modern browsers warn on plain HTTP for OIDC flows but they work. For a
production-grade setup, front Drasl with Caddy or Traefik using a local CA
cert (smallstep/step-ca pairs well with the existing homelab). For LAN-only
HTTP is fine.
### NeoForge version pinning ### NeoForge version pinning
Mods are picky about exact NeoForge minor versions. The compose currently Mods are picky about exact minor versions. Compose pins `NEOFORGE_VERSION:
pins `NEOFORGE_VERSION: "21.1.209"` as a placeholder. **Verify against "21.1.209"`. **Verify against https://projects.neoforged.net/neoforged/neoforge
https://projects.neoforged.net/neoforged/neoforge before deploying.** Don't before deploying.** Never `"latest"` for a stable server.
use `"latest"` for a stable server.
### Drasl username vs player name
- **Drasl username** = OIDC user's email (used for web UI login)
- **Player name** = `preferred_username` from OIDC, or user-chosen if
`AllowChoosingPlayerName = true`
- Keycloak users must have email set. Verify the realm's email-as-username
setting and the `preferred_username` mapper are configured.
### Mod source preference ### Mod source preference
Prefer **Modrinth over CurseForge** for itzg auto-download. CurseForge Prefer **Modrinth over CurseForge** (`packwiz modrinth add <slug>`). CF needs
requires an API key (`CF_API_KEY`) and has been historically flakier with `CF_API_KEY` and is flakier. Tag client-only mods `side = "client"` so the server
the itzg image. Use CF only for mods that are exclusive to it. doesn't pull and crash on them.
## Compose Stack
The working compose file is at `./docker-compose.yml`. Key environment vars
documented inline. Adjacent files:
```
.
├── docker-compose.yml
├── .env # RCON_PASSWORD, CF_API_KEY (if needed)
├── drasl/
│ └── config/
│ ├── config.toml # see ./drasl-config.toml example
│ └── keycloak-client-secret # single line, no trailing newline
├── extras/
│ ├── authlib-injector.jar # downloaded from yushijinhun's releases
│ ├── modrinth-mods.txt # one mod slug per line
│ └── cf-mods.txt # only if using CurseForge mods
├── backups/ # mc-backup writes here
└── CLAUDE.md # this file
```
### Memory sizing ### Memory sizing
With ~50100 mods and 8 concurrent players on 1.21.1: ~50100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
- `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G` is a safe starting point `USE_AIKAR_FLAGS: TRUE`.
- Use `USE_AIKAR_FLAGS: TRUE` for the well-known GC tuning
- Monitor with `mc-monitor` (itzg makes one) if you want metrics
### Backups ## Config (.env)
`itzg/mc-backup` runs alongside, talks to the server via RCON, takes
snapshots every 6h, prunes after 14 days. Backups are world-only (no mod
jars), which is correct — mods are reproducible from the mod list files.
## Keycloak Client Configuration `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT`/`CADDY_HTTP_BIND`,
`DISTRIBUTION_WEB_ROOT`, the Let's Encrypt block (`LE_EMAIL`, `LE_SUBDOMAINS`,
`OVH_*`), optional `CF_API_KEY`. Rendered configs (drasl, nmsr, packwiz) come
from `*.tmpl` via `tooling/render-config.sh` (substitutes `${BASE_DOMAIN}` only).
In the `homelab` realm (or whatever the realm is called): ## Client Distribution (guests)
1. **Clients → Create client** Launcher: **FjordLauncherUnlocked** (Prism fork, first-class authlib-injector),
- Client type: `OpenID Connect` mirrored at apex `/launcher/`. Per guest:
- Client ID: `drasl` 1. Download from `https://${BASE_DOMAIN}``/launcher/`.
- Name: `Drasl Minecraft Auth` 2. Add an authlib-injector account, URL
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
2. **Capability config** `https://auth.${BASE_DOMAIN}`).
- Client authentication: **ON** (confidential client) 3. Import the pack `https://pack.${BASE_DOMAIN}/pack.toml`.
- Authentication flow: **Standard flow** only 4. Connect to `${BASE_DOMAIN}` (`:25565`).
- Disable: Direct access grants, Implicit, Service accounts
3. **Login settings**
- Root URL: `http://drasl.home.local:25585`
- Valid redirect URIs: `http://drasl.home.local:25585/web/oidc-callback/Keycloak`
(the `Keycloak` at the end MUST match the `Name = "Keycloak"` in Drasl's
`[[RegistrationOIDC]]` block — case sensitive)
- Web origins: `http://drasl.home.local:25585`
4. **Credentials tab** → copy Client Secret to
`./drasl/config/keycloak-client-secret` (no trailing newline).
5. **Advanced tab** → PKCE Code Challenge Method: `S256`
## Client Distribution (LAN guests)
### Recommended launcher
**Prism Launcher** — open source, cross-platform (Win/Mac/Linux), first-class
authlib-injector support, can import/export instance ZIPs.
### Per-guest one-time setup
1. Install Prism Launcher.
2. Add authlib-injector account:
- URL: `http://drasl.home.local:25585/authlib-injector`
- Username/password: their Drasl credentials (or Minecraft Token if
registered via Keycloak OIDC)
3. Import the modpack instance ZIP.
### What to ship guests
- A Prism instance ZIP (right-click instance → Export Instance)
- A one-page README with the auth URL and import steps
- Optionally `authlib-injector.jar` for guests who refuse Prism
## Open / Pending Work ## Open / Pending Work
These were on the roadmap when we ended the brainstorm: - [ ] **Mod shortlist** for the kitchen-sink pack: Performance (Embeddium,
FerriteCore, ModernFix), Tech (Mekanism, Create, IE, AE2), Magic (Ars Nouveau,
- [ ] **Mod shortlist for the kitchen-sink pack.** Categories to fill: Botania, Iron's Spells), Storage (Sophisticated, Functional), Exploration
- Performance (Embeddium/Sodium-equivalent for NeoForge, FerriteCore, ModernFix) (YUNG's, Repurposed Structures), QoL (JEI, Jade, IPN, AppleSkin), World gen
- Tech/automation (Mekanism, Create, Immersive Engineering, AE2) (Tectonic, Terralith — verify NeoForge 1.21.1), Compat (Polymorph), Map (Xaero),
- Magic (Ars Nouveau, Botania, Iron's Spells 'n Spellbooks) Voice (Simple Voice Chat — 24454/udp already in compose).
- Storage (Sophisticated Storage/Backpacks, Functional Storage) - [ ] Server-side perf mods (C2ME, etc.).
- Exploration (YUNG's structures, Repurposed Structures, biome packs) - [ ] **Verify NeoForge version** against current stable before first deploy.
- QoL (JEI, Jade, JEI Resources, Inventory Profiles Next, AppleSkin) - [ ] **Bootstrap Drasl admin** account.
- World gen (Tectonic, Terralith — check NeoForge 1.21.1 compat)
- Compat glue (Polymorph for recipe conflicts)
- Map (XaeroMinimap + Xaero World Map)
- Voice (Simple Voice Chat — port 24454/udp already in compose)
- [ ] **Decide on dimension/server-side performance mods** (C2ME, Lithium-equivalent)
- [ ] **Reverse proxy + local TLS** for Drasl (Caddy + step-ca recommended,
given the existing homelab)
- [ ] **DNS record** in AdGuard for `drasl.home.local` → Docker host LAN IP
- [ ] **Keycloak client** creation per the section above
- [ ] **Verify NeoForge version** against current stable before first deploy
- [ ] **Bootstrap admin in Drasl**: leave `AllowPasswordLogin = true` initially,
create the admin account, then optionally disable password login to force OIDC
## Reference URLs ## Reference URLs
- Drasl repo: https://github.com/unmojang/drasl - Drasl: https://github.com/unmojang/drasl — config docs:
- Drasl configuration docs: https://github.com/unmojang/drasl/blob/master/doc/configuration.md https://github.com/unmojang/drasl/blob/master/doc/configuration.md
- Drasl recipes (example configs): https://github.com/unmojang/drasl/blob/master/doc/recipes.md
- authlib-injector: https://github.com/yushijinhun/authlib-injector - authlib-injector: https://github.com/yushijinhun/authlib-injector
- itzg/minecraft-server: https://github.com/itzg/docker-minecraft-server - itzg/minecraft-server: https://github.com/itzg/docker-minecraft-server
- itzg NeoForge docs: https://github.com/itzg/docker-minecraft-server/blob/master/docs/types-and-platforms/server-types/forge.md (NeoForge: …/docs/types-and-platforms/server-types/forge.md)
- itzg/mc-backup: https://github.com/itzg/docker-mc-backup - itzg/mc-backup: https://github.com/itzg/docker-mc-backup
- NeoForge versions: https://projects.neoforged.net/neoforged/neoforge - NeoForge versions: https://projects.neoforged.net/neoforged/neoforge
- Prism Launcher: https://prismlauncher.org/ - NMSR: https://github.com/NickAcPT/nmsr-rs
- Reference modpacks for inspiration (NOT forking): - Uptime Kuma: https://github.com/louislam/uptime-kuma
- ATM10 (Modrinth/CurseForge — ~500 mods, NeoForge 1.21.1) - FjordLauncherUnlocked: https://github.com/hero-persson/FjordLauncherUnlocked
- Leaking Kitchen Sink (CurseForge — trimmed ~150 mods, NeoForge 1.21.1) - Reference packs (inspiration, NOT forking): ATM10, Leaking Kitchen Sink.
## Communication Preferences (carry-over from past sessions) ## Communication Preferences (carry-over)
- Concise and direct - Concise and direct; diagnose + resolve without demanding repro steps.
- Diagnose and resolve without demanding detailed reproduction steps - Prefer iterative single-change updates over large rewrites.
- Prefer iterative single-change updates over large rewrites - Cautious scripts that halt on ambiguity rather than proceeding silently.
- Cautious scripts that halt on ambiguity rather than proceeding silently - Avoid over-engineering when a simple answer suffices.
- Avoid over-engineered responses when a simple answer suffices - Spanish or English both fine.
- Spanish or English fine; user is comfortable in both