Files
ulicraft-server-v1/README.md
Oier Bravo Urtasun 6df885eb13 docs(readme): document NMSR avatar renderer + ingress vhosts
Add an Avatar renderer (NMSR) section: Drasl-backed config, internal mcnet
resolution, localhost-only host port, endpoints, lavapipe note. List nmsr +
distribution as optional layers, refresh the Public TLS vhost list (www/avatar,
HTTPS-only), and update the Layout tree.

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

191 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ulicraft Server
Self-hosted modded Minecraft (NeoForge 1.21.1) for a LAN party. Runs **online**
(internet present) or **fully air-gapped** (offline). Self-hosted auth/skins
(Drasl), a packwiz modpack, a guest landing page, and a transparent mirror of
Mojang/launcher/NeoForge assets so guest laptops need no internet.
## Stack
| Service | Image | Role |
|---|---|---|
| `minecraft` | itzg/minecraft-server | NeoForge 1.21.1 server, `:25565` |
| `drasl` | unmojang/drasl | Yggdrasil auth + skins (password login) |
| `caddy` | caddy:alpine | ingress: landing, auth/packwiz proxy, asset mirror (`tls internal`) |
| `mc-backup` | itzg/mc-backup | world backups every 6h via RCON |
Optional layers (each its own `docker-compose.<name>.yml`):
- `avahi` (mDNS `.local`), `dnsmasq` (turnkey DNS) — only if you don't run your
own DNS.
- `nmsr` (NickAcPT/nmsr-rs, built from source) — skin/avatar renderer at
`avatar.${BASE_DOMAIN}`, sourced from Drasl. See **Avatar renderer** below.
- `distribution` — extra static vhost at `distribution.${BASE_DOMAIN}` whose web
root (`DISTRIBUTION_WEB_ROOT`) lives in another repo.
Config lives in `.env`: `BASE_DOMAIN` (default `ulicraft.lan`), `HOST_LAN_IP`,
`RCON_PASSWORD`, `ENABLE_MDNS`. Services are subdomains: `auth.`, `packwiz.`,
`mc.`, plus the apex landing page.
## DNS (pick one)
Names must resolve to `HOST_LAN_IP`. Use **your own party DNS server** (recommended):
```sh
tooling/dns-records.sh online # service names only (internet present)
tooling/dns-records.sh airgap # + Mojang/launcher/NeoForge spoofs (offline)
```
Paste the printed records into your DNS. **Use `.lan`, not `.local`**`.local`
is intercepted by mDNS and won't resolve via unicast DNS.
Alternatives: `ENABLE_MDNS=true` + `BASE_DOMAIN=*.local` (zero-config mDNS,
macOS/Linux native, Windows needs Bonjour) — but mDNS can't serve the air-gap
upstream spoofs. Or layer in `docker-compose.dnsmasq.yml` for a turnkey DNS.
### Simulate the DNS via /etc/hosts (single machine, testing)
No DNS server? Append the records straight to `/etc/hosts` (marker-wrapped):
```sh
tooling/dns-records.sh online hosts | sudo tee -a /etc/hosts # services only
tooling/dns-records.sh airgap hosts | sudo tee -a /etc/hosts # + Mojang spoofs
```
Remove them later:
```sh
sudo sed -i '/# >>> ulicraft/,/# <<< ulicraft/d' /etc/hosts
```
Notes:
- `/etc/hosts` has **no wildcard** — only the listed names resolve.
- `airgap` adds the Mojang/launcher/NeoForge hostnames pointing at the LAN; this
**breaks normal internet access to those domains** while present. Use it only
while offline; for an online box use `online`.
- `mc.<domain>` works (client defaults to :25565); SRV isn't used via `/etc/hosts`.
## Prerequisites
Docker + Docker Compose; for prep also `pnpm`, `packwiz`, `jq`, `curl`,
`envsubst`, `sha1sum`.
## Launch
```sh
cp .env.example .env # set BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD
tooling/build-stack.sh --prep # ONLINE, once: render, build landing,
# mirror mods+assets, fetch launcher +
# authlib, pre-bake the server volume
tooling/build-stack.sh --up online # internet present, no mirror
tooling/build-stack.sh --up airgap # offline; full asset mirror on :443 (default)
tooling/build-stack.sh --up core # base only (debugging)
```
`--up` prints the DNS records to add for that mode. Down:
`docker compose -f docker-compose.yml -f docker-compose.static.yml -f docker-compose.mirror.yml down`
## Public TLS (host nginx in front of caddy)
The LAN path above runs caddy on `:80`. To expose the stack publicly with real
certs, the machine's own nginx terminates TLS and reverse-proxies every vhost to
caddy by Host header. Caddy stays the single router (apex landing, `www.`
apex, `auth.` → drasl, `pack.` packwiz, `distribution.` static, `avatar.`
nmsr). HTTPS-only: HTTP 301s to HTTPS and every TLS vhost sends HSTS.
1. **Issue certs** (OVH DNS-01, no inbound ports needed):
```sh
# .env: BASE_DOMAIN, LE_EMAIL, OVH_* creds,
# LE_SUBDOMAINS="auth pack distribution www avatar"
tooling/issue-letsencrypt.sh # → certs/<name>/{cert,key}.pem
```
2. **Bind caddy to a localhost-only port** so only nginx reaches it (`.env`):
```sh
CADDY_HTTP_PORT=8880
CADDY_HTTP_BIND=127.0.0.1
```
3. **Bring up the caddy ingress** (+ static, + distribution/nmsr if used):
```sh
docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
-f docker-compose.static.yml -f docker-compose.distribution.yml \
-f docker-compose.nmsr.yml up -d --build
```
4. **Render + install the nginx vhost** with `tooling/render-nginx.sh`. It pulls
`BASE_DOMAIN` + `CADDY_HTTP_PORT` from `.env`, defaults `APP_DIR` to the repo
checkout (where `certs/` live), and renders `nginx/ulicraft-caddy.conf.tmpl`:
```sh
tooling/render-nginx.sh # preview to stdout (dry run)
tooling/render-nginx.sh --install # write, symlink, nginx -t, reload
```
Overrides via env: `APP_DIR=/path` (if the repo isn't at the cert location),
`SITE_NAME=foo.conf`, `TEMPLATE=nginx/ulicraft.conf.tmpl` (the static variant —
nginx serves the files itself + proxies only drasl; pick one, not both).
Re-run step 4 whenever you add a subdomain so its server block is rendered.
## Avatar renderer (NMSR)
`avatar.${BASE_DOMAIN}` renders players' skins/avatars via
[NMSR-aas](https://github.com/NickAcPT/nmsr-rs), sourced from **Drasl** (not
Mojang). No upstream image exists, so it's built from source — `docker/nmsr/`
pins a commit; bump `ARG NMSR_REF` to update.
- **Config**: `nmsr/config.toml.tmpl` → rendered to `nmsr/config.toml` by
`tooling/render-config.sh`. `[mojank]` points every Mojang endpoint at
`http://auth.${BASE_DOMAIN}`; Drasl serves the Mojang-compatible routes at its
bare BaseURL and embeds absolute skin URLs that NMSR fetches directly.
`allow_offline_mode_uuids = true` (Drasl issues offline v3 UUIDs).
- **Network**: skin resolution stays internal over `mcnet` (the caddy alias
`auth.${BASE_DOMAIN}` → drasl) — no public round-trip. caddy reverse-proxies
`avatar.` → `nmsr:8080`. The host port is published on `127.0.0.1:9898` only
(local debugging; not reachable from outside).
- **Endpoints** (NMSR): e.g. `https://avatar.${BASE_DOMAIN}/skin/<player>`,
`/face/<player>`, `/fullbody/<player>` — by username or UUID.
Bring up: add `-f docker-compose.nmsr.yml` (with `--build`) as shown above. The
first build is heavy (Rust compile). Headless rendering uses lavapipe (software
Vulkan); if renders fail, check `docker logs nmsr` for Vulkan device init.
`distribution.${BASE_DOMAIN}` serves a static site whose web root lives in
another repo: set `DISTRIBUTION_WEB_ROOT` (absolute host path) in `.env`; it's
bind-mounted read-only via `docker-compose.distribution.yml`.
## Join (guests)
1. Open `http://ulicraft.lan`, download FjordLauncherUnlocked for your OS.
2. Add an **authlib-injector** account, URL `http://auth.ulicraft.lan/authlib-injector`
(register/login at `http://auth.ulicraft.lan`).
3. **Air-gap only:** trust the local CA — download `http://ulicraft.lan/ca.crt`
and add it to your OS trust store (so the HTTPS asset mirror is trusted).
4. Import the pack: `http://packwiz.ulicraft.lan/pack.toml`.
5. Connect to **`mc.ulicraft.lan`** (no port needed).
## Layout
```
docker-compose.yml # core: drasl, minecraft, mc-backup, caddy
.static.yml .mirror.yml # online/airgap layers
.caddy.yml .nginx.yml # ingress paths (caddy-front vs nginx static)
.nmsr.yml .distribution.yml # avatar renderer / extra static vhost
.avahi.yml .dnsmasq.yml # optional DNS layers
caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets (core/static/mirror/
# distribution/avatar)
drasl/config/config.toml.tmpl # auth config (rendered)
nmsr/config.toml.tmpl # avatar renderer config, Drasl-backed (rendered)
dnsmasq/dnsmasq.conf.tmpl # optional DNS config (rendered)
docker/avahi/ docker/nmsr/ # built-from-source images (mDNS, NMSR)
pack/ # packwiz modpack source
landing/ # Astro site → www/
tooling/ # build-stack, render-config, render-nginx,
# dns-records, issue-letsencrypt,
# mirror-mods, mirror-airgap, fetch-launcher
nginx/*.conf.tmpl # host-nginx TLS vhost templates
mirror/ # vendored jars + launcher + asset mirror (gitignored)
plan/ # full design docs (0012)
```
See `plan/00-overview.md` for the full design.