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
> persistent auth/skin infrastructure designed to outlive the LAN weekend
> and integrate with the existing homelab.
> Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
> (Drasl), a packwiz 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, LAN-based
- **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
- **Mod loader**: NeoForge (NOT classic Forge — see Decisions below)
- **Network**: LAN party context, but stack is persistent (lives past the weekend)
- **Auth**: Self-hosted Yggdrasil-compatible (Drasl) + Keycloak OIDC
- **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`.
- **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
```
LAN segment
├── AdGuard Home → resolves drasl.home.local, keycloak.home.local
│ 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
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.`).
Clients (LAN party guests):
Prism Launcher → authlib-injector account pointing at Drasl
→ joins minecraft:25565
```
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)
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.
2. Clicks "Register with Keycloak" → redirected to Keycloak login.
3. Logs in with homelab Keycloak credentials.
4. Returned to Drasl, picks a player name, uploads a skin.
5. Drasl shows them a "Minecraft Token" on their profile page.
6. In Prism Launcher: Settings → Accounts → Add authlib-injector account
with URL `http://drasl.home.local:25585/authlib-injector` and the
Minecraft Token as password.
7. Joins Minecraft server. Server validates session against Drasl (via
authlib-injector JVM agent), Drasl confirms, player enters with their skin.
## 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.
- **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
### Minecraft 1.21+ secure profile incompatibility
- Server: must set `enforce-secure-profile=false` (compose: `ENFORCE_SECURE_PROFILE=FALSE`)
- Drasl: must set `SignPublicKeys = false`
- These are linked. The Drasl docs explicitly warn: *"Mixed authentication does
not work with `SignPublicKeys = true` on Minecraft 1.21+."*
### 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:/path/to/authlib-injector.jar=<yggdrasil-api-url>`
- The URL after `=` is the **API root**, which for Drasl is
`<BaseURL>/authlib-injector`.
- **Must match between server and client.** If server uses
`http://drasl.home.local:25585/authlib-injector` and client uses anything
else (e.g. an IP), session validation fails silently with "Invalid session".
- Download from: https://github.com/yushijinhun/authlib-injector/releases
- Mount into the itzg container 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.
- 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 NeoForge minor versions. The compose currently
pins `NEOFORGE_VERSION: "21.1.209"` as a placeholder. **Verify against
https://projects.neoforged.net/neoforged/neoforge before deploying.** Don't
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.
Mods are picky about exact minor versions. Compose pins `NEOFORGE_VERSION:
"21.1.209"`. **Verify against https://projects.neoforged.net/neoforged/neoforge
before deploying.** Never `"latest"` for a stable server.
### Mod source preference
Prefer **Modrinth over CurseForge** for itzg auto-download. CurseForge
requires an API key (`CF_API_KEY`) and has been historically flakier with
the itzg image. Use CF only for mods that are exclusive to it.
## 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
```
Prefer **Modrinth over CurseForge** (`packwiz modrinth add <slug>`). CF needs
`CF_API_KEY` and is flakier. Tag client-only mods `side = "client"` so the server
doesn't pull and crash on them.
### Memory sizing
With ~50100 mods and 8 concurrent players on 1.21.1:
- `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G` is a safe starting point
- Use `USE_AIKAR_FLAGS: TRUE` for the well-known GC tuning
- Monitor with `mc-monitor` (itzg makes one) if you want metrics
~50100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
`USE_AIKAR_FLAGS: TRUE`.
### Backups
`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.
## Config (.env)
## 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**
- Client type: `OpenID Connect`
- Client ID: `drasl`
- Name: `Drasl Minecraft Auth`
2. **Capability config**
- Client authentication: **ON** (confidential client)
- Authentication flow: **Standard flow** only
- 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
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. Import the pack `https://pack.${BASE_DOMAIN}/pack.toml`.
4. Connect to `${BASE_DOMAIN}` (`:25565`).
## Open / Pending Work
These were on the roadmap when we ended the brainstorm:
- [ ] **Mod shortlist for the kitchen-sink pack.** Categories to fill:
- Performance (Embeddium/Sodium-equivalent for NeoForge, FerriteCore, ModernFix)
- Tech/automation (Mekanism, Create, Immersive Engineering, AE2)
- Magic (Ars Nouveau, Botania, Iron's Spells 'n Spellbooks)
- Storage (Sophisticated Storage/Backpacks, Functional Storage)
- Exploration (YUNG's structures, Repurposed Structures, biome packs)
- QoL (JEI, Jade, JEI Resources, Inventory Profiles Next, AppleSkin)
- 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
- [ ] **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
- Drasl repo: https://github.com/unmojang/drasl
- Drasl configuration docs: https://github.com/unmojang/drasl/blob/master/doc/configuration.md
- Drasl recipes (example configs): https://github.com/unmojang/drasl/blob/master/doc/recipes.md
- 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
- 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
- NeoForge versions: https://projects.neoforged.net/neoforged/neoforge
- Prism Launcher: https://prismlauncher.org/
- Reference modpacks for inspiration (NOT forking):
- ATM10 (Modrinth/CurseForge — ~500 mods, NeoForge 1.21.1)
- Leaking Kitchen Sink (CurseForge — trimmed ~150 mods, NeoForge 1.21.1)
- 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 from past sessions)
## Communication Preferences (carry-over)
- Concise and direct
- Diagnose and resolve without demanding detailed reproduction steps
- Prefer iterative single-change updates over large rewrites
- Cautious scripts that halt on ambiguity rather than proceeding silently
- Avoid over-engineered responses when a simple answer suffices
- Spanish or English fine; user is comfortable in both
- 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.