refactor!: unify into one compose, drop airgap/mirror/dnsmasq/avahi/blessingskin
Collapse the override-file matrix into a single docker-compose.yml holding the whole stack: drasl, minecraft, mc-backup, caddy, nmsr, uptime-kuma. Bring-up is now just `docker compose up -d --build`. Removed features (dead-ends for this deployment): - airgap / full asset mirror (mirror-airgap.sh, mirror-mods.sh, 20-mirror.caddy, caddy local-CA export, /ca.crt) - dnsmasq + avahi/mDNS + dns-records.sh + mdns-host-setup.sh + ENABLE_MDNS; DNS is now handled outside this repo (HOST_LAN_IP dropped) - Blessing Skin auth variant (compose + 00-core-blessingskin.caddy + BS_* env) - host-nginx-static variant (docker-compose.nginx.yml, nginx/ulicraft.conf.tmpl) - build-stack.sh and its run modes (online/airgap/core) Production ingress is the only path now: host nginx terminates TLS (LE certs) in front of caddy on a localhost-only port; caddy routes every vhost by Host header. Launcher downloads move mirror/launcher -> launcher/. Docs: README, deploy skill, and plan/ rewritten to match; obsolete plan docs (dnsmasq, blessing-skin, assets-mirror, full-airgap-mirror, dns-and-run-modes) deleted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
203
README.md
203
README.md
@@ -1,99 +1,70 @@
|
||||
# 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.
|
||||
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 | ingress: landing, auth/packwiz proxy, asset mirror (`tls internal`) |
|
||||
| `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 |
|
||||
|
||||
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.
|
||||
- `status` (louislam/uptime-kuma) — uptime monitoring + public status page at
|
||||
`status.${BASE_DOMAIN}`. See **Status monitoring** below.
|
||||
Vhosts (all proxied through the host nginx → caddy):
|
||||
|
||||
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.
|
||||
- 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`)
|
||||
|
||||
## DNS (pick one)
|
||||
Config lives in `.env`: `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT` /
|
||||
`CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`. See `.env.example`.
|
||||
|
||||
Names must resolve to `HOST_LAN_IP`. Use **your own party DNS server** (recommended):
|
||||
## DNS
|
||||
|
||||
```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`.
|
||||
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`, `sha1sum`.
|
||||
`envsubst`.
|
||||
|
||||
## Launch
|
||||
|
||||
```sh
|
||||
cp .env.example .env # set BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD
|
||||
cp .env.example .env # set BASE_DOMAIN, RCON_PASSWORD, DISTRIBUTION_WEB_ROOT
|
||||
|
||||
tooling/build-stack.sh --prep # ONLINE, once: render, build landing,
|
||||
# mirror mods+assets, fetch launcher +
|
||||
# authlib, pre-bake the server volume
|
||||
# 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/)
|
||||
# authlib-injector.jar → runtime/ (server JVM agent; see drasl docs)
|
||||
|
||||
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)
|
||||
docker compose up -d --build # first run installs NeoForge + mods, builds nmsr
|
||||
docker compose down # stop
|
||||
```
|
||||
|
||||
`--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`
|
||||
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)
|
||||
|
||||
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.
|
||||
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
|
||||
@@ -102,31 +73,15 @@ nmsr). HTTPS-only: HTTP 301s to HTTPS and every TLS vhost sends HSTS.
|
||||
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 -f docker-compose.status.yml up -d --build
|
||||
```
|
||||
|
||||
4. **Render + install the nginx vhost** with `tooling/render-nginx.sh`. It pulls
|
||||
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
|
||||
```
|
||||
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.
|
||||
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)
|
||||
|
||||
@@ -140,40 +95,25 @@ pins a commit; bump `ARG NMSR_REF` to update.
|
||||
`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>`,
|
||||
- **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/<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
|
||||
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`.
|
||||
|
||||
## 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. The container joins `mcnet`, so it
|
||||
can probe the stack by internal service name without leaving the host. caddy
|
||||
reverse-proxies `status.` → `uptime-kuma:3001`; the host port is published on
|
||||
`127.0.0.1:3001` only (first-run admin setup / local debugging).
|
||||
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`.
|
||||
|
||||
Bring up (layer on the caddy ingress):
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
||||
-f docker-compose.status.yml up -d
|
||||
```
|
||||
|
||||
Then open `https://status.${BASE_DOMAIN}` (or `http://127.0.0.1:3001` first run),
|
||||
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):
|
||||
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 |
|
||||
|---|---|---|
|
||||
@@ -185,42 +125,35 @@ these once via the UI (data persists in the `kuma_data` volume):
|
||||
| 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. Mix
|
||||
as you like. `status` is in the default `LE_SUBDOMAINS` so its TLS cert issues
|
||||
with the rest; the nginx vhost adds websocket upgrade headers for Kuma's live UI.
|
||||
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 `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).
|
||||
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 # 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)
|
||||
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)
|
||||
dnsmasq/dnsmasq.conf.tmpl # optional DNS config (rendered)
|
||||
docker/avahi/ docker/nmsr/ # built-from-source images (mDNS, NMSR)
|
||||
docker/nmsr/ # built-from-source NMSR image
|
||||
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 (00–12)
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user