# Ulicraft Server 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. One unified `docker-compose.yml` runs the whole stack behind the host's nginx + Let's Encrypt. ## Stack One compose file, one `docker compose up -d`: | Service | Image | Role | |---|---|---| | `minecraft` | itzg/minecraft-server | NeoForge 1.21.1 server, `:25565` | | `drasl` | unmojang/drasl | Yggdrasil auth + skins (password login) | | `caddy` | caddy:alpine | internal ingress: routes every vhost by Host header | | `nmsr` | built from source | skin/avatar renderer at `avatar.${BASE_DOMAIN}` | | `uptime-kuma` | louislam/uptime-kuma | status page at `status.${BASE_DOMAIN}` | | `mc-backup` | itzg/mc-backup | world backups every 6h via RCON | Vhosts (all proxied through the host nginx → caddy): - apex `${BASE_DOMAIN}` — landing page + `/launcher/` downloads - `auth.` — Drasl - `pack.` — packwiz metadata + `/custom/` jars - `avatar.` — NMSR - `status.` — Uptime Kuma - `distribution.` — static site from another repo (`DISTRIBUTION_WEB_ROOT`) Config lives in `.env`: `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT` / `CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`. See `.env.example`. ## DNS Handled **outside this repo**. Point `${BASE_DOMAIN}` and every subdomain (`auth. pack. avatar. status. distribution. www.`) at the host running nginx. ## Prerequisites Docker + Docker Compose; for prep also `pnpm`, `packwiz`, `jq`, `curl`, `envsubst`. ## Launch ```sh cp .env.example .env # set BASE_DOMAIN, RCON_PASSWORD, DISTRIBUTION_WEB_ROOT # One-time / on change — render configs + build content: tooling/render-config.sh # drasl + nmsr + packwiz configs from *.tmpl ( cd landing && pnpm install && BASE_DOMAIN="$BASE_DOMAIN" pnpm run build ) # → www/ tooling/fetch-launcher.sh # FjordLauncher assets → launcher/ (served at /launcher/) tooling/fetch-authlib.sh # authlib-injector.jar → runtime/ (server JVM agent) docker compose up -d --build # first run installs NeoForge + mods, builds nmsr docker compose down # stop ``` First boot: itzg installs NeoForge + the packwiz mods into the `mc_data` volume; nmsr does a one-time Rust compile. Watch `docker compose logs -f minecraft` until `Done`. ## Public TLS (host nginx in front of caddy) caddy is published on a localhost-only port (`CADDY_HTTP_BIND=127.0.0.1`, `CADDY_HTTP_PORT=8880`). The host's own nginx terminates TLS with Let's Encrypt certs and reverse-proxies every vhost to caddy by Host header. 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 status" tooling/issue-letsencrypt.sh # → certs//{cert,key}.pem ``` 2. **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 ``` Override `APP_DIR=/path` if the repo isn't at the cert location. Re-run 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` (caddy alias `auth.${BASE_DOMAIN}` → drasl). caddy reverse-proxies `avatar.` → `nmsr:8080`. - **Endpoints**: e.g. `https://avatar.${BASE_DOMAIN}/skin/`, `/face/`, `/fullbody/` — by username or UUID. First build is heavy (Rust compile). Headless rendering uses lavapipe (software Vulkan); if renders fail, check `docker logs nmsr` for Vulkan device init. ## Status monitoring (Uptime Kuma) `status.${BASE_DOMAIN}` runs [Uptime Kuma](https://github.com/louislam/uptime-kuma) — uptime probes for every vhost **and** a native Minecraft-protocol ping (player count + up/down), plus a public status page. It joins `mcnet`, so monitors can probe the stack by internal service name. caddy reverse-proxies `status.` → `uptime-kuma:3001`. First run, open `https://status.${BASE_DOMAIN}`, create the admin account, and add monitors. Kuma has no config-as-code — add these once via the UI (data persists in the `kuma_data` volume): | Monitor | Type | Target | |---|---|---| | Minecraft | Minecraft Server | `minecraft` : `25565` | | Drasl auth | HTTP(s) | `https://auth.${BASE_DOMAIN}` | | Landing (apex) | HTTP(s) | `https://${BASE_DOMAIN}` | | Packwiz | HTTP(s) | `https://pack.${BASE_DOMAIN}` | | Distribution | HTTP(s) | `https://distribution.${BASE_DOMAIN}` | | Avatar (NMSR) | HTTP(s) | `https://avatar.${BASE_DOMAIN}` | Internal-name targets (`minecraft`, `drasl:25585`, `nmsr:8080`) isolate "service down" from "ingress/TLS/DNS down"; public-URL targets test the whole chain. `status` is in the default `LE_SUBDOMAINS`; the nginx vhost adds websocket upgrade headers for Kuma's live UI. ## Join (guests) 1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS. 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}`** (Minecraft, `:25565`). ## Layout ``` docker-compose.yml # the whole stack (one file) caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets # (core/static/distribution/avatar/status) drasl/config/config.toml.tmpl # auth config (rendered) nmsr/config.toml.tmpl # avatar renderer config, Drasl-backed (rendered) docker/nmsr/ # built-from-source NMSR image pack/ # packwiz modpack source landing/ # Astro site → www/ launcher/ # FjordLauncher assets (gitignored) tooling/ # render-config, render-pack, render-nginx, # issue-letsencrypt, fetch-launcher, add-custom-mod nginx/ulicraft-caddy.conf.tmpl # host-nginx TLS vhost template (front of caddy) plan/ # design docs ``` See `plan/00-overview.md` for the full design.