Compare commits
17 Commits
landing
...
067e990306
| Author | SHA1 | Date | |
|---|---|---|---|
| 067e990306 | |||
| 673202e1e4 | |||
| 1ec3a9c1ee | |||
| 663b87a4e4 | |||
| a2821f2bbc | |||
| cfd60131fb | |||
| 95c63e1ee0 | |||
| 46103495fb | |||
| 1709bcd340 | |||
| 67d1f82e6a | |||
| ff645de2c8 | |||
| 6df885eb13 | |||
| 6fac1647aa | |||
| a6155819b6 | |||
| 3185fea4c1 | |||
| f3c38da56c | |||
| 21e6080e78 |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: deploy
|
||||
description: Redeploy the Ulicraft Minecraft stack to the production host `cochi` — git pull on main then a hard restart (compose down + build-stack.sh --up online). Use when the user says "deploy", "redeploy", "deploy to cochi", "push to prod", "ship it", or "restart the server" for this project. Causes brief Minecraft downtime.
|
||||
description: Redeploy the Ulicraft Minecraft stack to the production host `cochi` — git pull on main then a hard restart (compose down + up). Use when the user says "deploy", "redeploy", "deploy to cochi", "push to prod", "ship it", or "restart the server" for this project. Causes brief Minecraft downtime.
|
||||
---
|
||||
|
||||
# Deploy Ulicraft to `cochi`
|
||||
@@ -13,8 +13,9 @@ Redeploy the running stack on the production host. Full reference:
|
||||
- Host: `cochi` (SSH alias)
|
||||
- Path: `/home/ubuntu/mc/ulicraft-server-v1`
|
||||
- Branch: `main`
|
||||
- Run mode: `online` → compose files `docker-compose.yml` + `docker-compose.static.yml`
|
||||
- Auth: Drasl (base stack)
|
||||
- Stack: single unified `docker-compose.yml` (drasl, minecraft, mc-backup,
|
||||
caddy, nmsr, uptime-kuma)
|
||||
- Auth: Drasl
|
||||
- Restart: **hard** (down → up) — players are disconnected for a few minutes
|
||||
|
||||
## Procedure
|
||||
@@ -35,32 +36,36 @@ invoking this skill is the authorization to proceed, but:
|
||||
ssh cochi 'set -e
|
||||
cd /home/ubuntu/mc/ulicraft-server-v1
|
||||
git pull --ff-only
|
||||
docker compose -f docker-compose.yml -f docker-compose.static.yml down --remove-orphans
|
||||
tooling/build-stack.sh --up online'
|
||||
tooling/render-config.sh
|
||||
tooling/fetch-authlib.sh
|
||||
docker compose down --remove-orphans
|
||||
docker compose up -d --build'
|
||||
```
|
||||
- `--ff-only` refuses to merge — if the pull is not a fast-forward, STOP and
|
||||
report (local changes on the host need manual resolution).
|
||||
- `build-stack.sh --up online` renders configs (needs `.env` complete:
|
||||
`BASE_DOMAIN`, `HOST_LAN_IP`, `RCON_PASSWORD`) and runs `compose up -d`.
|
||||
- `render-config.sh` re-renders drasl/nmsr/packwiz configs from `.env`
|
||||
(needs `.env` complete: `BASE_DOMAIN`, `RCON_PASSWORD`,
|
||||
`DISTRIBUTION_WEB_ROOT`, `CADDY_HTTP_*`).
|
||||
- `fetch-authlib.sh` ensures `runtime/authlib-injector.jar` exists (gitignored;
|
||||
skips if already valid). Missing jar = minecraft crashloops with "Error
|
||||
opening zip file or JAR manifest missing".
|
||||
|
||||
3. **Verify.**
|
||||
```bash
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && docker compose -f docker-compose.yml -f docker-compose.static.yml ps'
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && docker compose ps'
|
||||
```
|
||||
Confirm `caddy`, `drasl`, `minecraft`, `mc-backup` are Up. Tail the minecraft
|
||||
log briefly and confirm it reaches `Done` (server ready):
|
||||
Confirm `caddy`, `drasl`, `minecraft`, `mc-backup`, `nmsr`, `uptime-kuma` are
|
||||
Up. Tail the minecraft log briefly and confirm it reaches `Done` (server
|
||||
ready):
|
||||
```bash
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && timeout 120 docker compose -f docker-compose.yml -f docker-compose.static.yml logs -f minecraft' 2>/dev/null | grep -m1 "Done"
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && timeout 120 docker compose logs -f minecraft' 2>/dev/null | grep -m1 "Done"
|
||||
```
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Never `git reset --hard` or discard changes on `cochi` without telling the user
|
||||
first — there may be host-local edits.
|
||||
- Volumes (`mc_data`, `drasl_state`) persist across `down`; the world is safe. Do
|
||||
NOT pass `-v`/`--volumes` to `down`.
|
||||
- Volumes (`mc_data`, `drasl_state`, `kuma_data`) persist across `down`; the
|
||||
world and monitors are safe. Do NOT pass `-v`/`--volumes` to `down`.
|
||||
- If a step fails, STOP and surface the exact error + the failing command. Do not
|
||||
improvise recovery on production.
|
||||
- Blessing Skin variant or `airgap` mode changes the compose file set — if the
|
||||
user asks for those, follow `plan/14-deploy.md` instead of the fixed commands
|
||||
above.
|
||||
|
||||
47
.env.example
47
.env.example
@@ -1,35 +1,22 @@
|
||||
# Copy to .env and fill in real values
|
||||
|
||||
# Single base domain for the whole stack; every service is a subdomain of this.
|
||||
# Use .lan (NOT .local) — .local is intercepted by mDNS on macOS/Linux and
|
||||
# won't resolve through a normal unicast DNS server.
|
||||
BASE_DOMAIN=ulicraft.lan
|
||||
# The Docker host's LAN IP — every service name resolves here.
|
||||
HOST_LAN_IP=192.168.1.10
|
||||
|
||||
# mDNS toggle. true + BASE_DOMAIN=*.local → an Avahi container publishes the
|
||||
# service names over mDNS (zero-config for macOS/Linux guests; Windows needs
|
||||
# Bonjour). false → use your own party DNS (records: tooling/dns-records.sh).
|
||||
# NOTE: mDNS resolves ONLY *.local; it cannot serve the air-gap upstream-host
|
||||
# spoofs — those always need a real DNS server.
|
||||
ENABLE_MDNS=false
|
||||
# DNS is handled OUTSIDE this repo — point ${BASE_DOMAIN} and every subdomain
|
||||
# (auth. pack. avatar. status. distribution. www.) at the host running nginx.
|
||||
BASE_DOMAIN=ulicraft.net
|
||||
|
||||
RCON_PASSWORD=change-me-to-something-random
|
||||
|
||||
# Caddy host-published port (docker-compose.caddy.yml). Default 80 for the
|
||||
# standalone LAN/air-gap path. When the machine's own nginx fronts caddy
|
||||
# (nginx terminates TLS → reverse-proxies to caddy), set a high port and bind
|
||||
# it to localhost so only nginx reaches it:
|
||||
# CADDY_HTTP_PORT=8880
|
||||
# CADDY_HTTP_BIND=127.0.0.1
|
||||
# Then render nginx/ulicraft-caddy.conf.tmpl with CADDY_HTTP_PORT.
|
||||
CADDY_HTTP_PORT=80
|
||||
CADDY_HTTP_BIND=0.0.0.0
|
||||
# caddy host-published port. The host's own nginx terminates TLS and
|
||||
# reverse-proxies to caddy by Host header (nginx/ulicraft-caddy.conf.tmpl), so
|
||||
# bind caddy to a high localhost-only port — only nginx should reach it.
|
||||
CADDY_HTTP_PORT=8880
|
||||
CADDY_HTTP_BIND=127.0.0.1
|
||||
|
||||
# distribution.${BASE_DOMAIN} static vhost (docker-compose.distribution.yml).
|
||||
# Absolute host path to the built static site — it lives in ANOTHER repo.
|
||||
# Bind-mounted read-only into caddy and served at distribution.${BASE_DOMAIN}.
|
||||
# distribution.${BASE_DOMAIN} static vhost. Absolute host path to the built
|
||||
# static site — it lives in ANOTHER repo. Bind-mounted read-only into caddy.
|
||||
DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distribution/dist
|
||||
|
||||
# Only needed if using CurseForge-exclusive mods:
|
||||
# CF_API_KEY=your-curseforge-api-key
|
||||
|
||||
@@ -37,7 +24,7 @@ DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distri
|
||||
# Only needed to issue real TLS certs. DNS-01 needs no inbound ports.
|
||||
LE_EMAIL=you@example.com
|
||||
# space-separated subdomains; apex ${BASE_DOMAIN} is always included
|
||||
LE_SUBDOMAINS=auth pack distribution
|
||||
LE_SUBDOMAINS=auth pack distribution www avatar status
|
||||
# OVH API creds (DNS zone write). Create at https://eu.api.ovh.com/createToken/
|
||||
# OVH_CK can be blank on first run — acme.sh prints an auth URL, then paste it.
|
||||
OVH_END_POINT=ovh-eu
|
||||
@@ -45,13 +32,3 @@ OVH_AK=
|
||||
OVH_AS=
|
||||
OVH_CK=
|
||||
# LE_STAGING=1 # use the untrusted staging CA for dry runs
|
||||
|
||||
# ── Blessing Skin auth variant (docker-compose.blessingskin.yml) ──────
|
||||
# Only needed when running the Blessing Skin override instead of Drasl.
|
||||
BS_DB_NAME=blessingskin
|
||||
BS_DB_USER=blessingskin
|
||||
BS_DB_PASSWORD=change-me-bs-db
|
||||
BS_DB_ROOT_PASSWORD=change-me-bs-root
|
||||
# Generate once and paste here so it survives container recreates:
|
||||
# docker run --rm azusamikan/blessing-skin-server-docker:latest php artisan key:generate --show
|
||||
BS_APP_KEY=base64:replace-with-generated-key
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,15 +5,15 @@ runtime/authlib-injector.jar
|
||||
|
||||
# rendered configs (from tooling/render-config.sh)
|
||||
drasl/config/config.toml
|
||||
dnsmasq/dnsmasq.conf
|
||||
nmsr/config.toml
|
||||
# packwiz pack files rendered from pack/**/*.tmpl by tooling/render-pack.sh
|
||||
# (domain-dependent build output); source of truth is the .tmpl + custom/ jars.
|
||||
pack/mods/*.pw.toml
|
||||
pack/index.toml
|
||||
pack/CustomSkinLoader/CustomSkinLoader.json
|
||||
|
||||
# vendored / mirrored binaries
|
||||
mirror/
|
||||
# vendored binaries
|
||||
launcher/
|
||||
pack/mods-files/
|
||||
|
||||
# landing page: generated build output + deps
|
||||
@@ -22,8 +22,5 @@ landing/node_modules/
|
||||
landing/.astro/
|
||||
landing/dist/
|
||||
|
||||
# exported Caddy CA (per-deploy artifact)
|
||||
caddy/caddy-root-ca.crt
|
||||
|
||||
# Let's Encrypt certs + private keys (issued by tooling/issue-letsencrypt.sh)
|
||||
certs/
|
||||
|
||||
343
CLAUDE.md
343
CLAUDE.md
@@ -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**: 4–10 friends, LAN-based
|
||||
- **Players**: 4–10 friends
|
||||
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
|
||||
- **Pack approach**: Manually curated (~50–100 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 ~50–100 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
|
||||
~50–100 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.
|
||||
|
||||
190
README.md
190
README.md
@@ -1,111 +1,159 @@
|
||||
# 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: `avahi` (mDNS `.local`), `dnsmasq` (turnkey DNS) — only if you
|
||||
don't run your own DNS.
|
||||
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/)
|
||||
tooling/fetch-authlib.sh # authlib-injector.jar → runtime/ (server JVM agent)
|
||||
|
||||
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)
|
||||
|
||||
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/<name>/{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/<player>`,
|
||||
`/face/<player>`, `/fullbody/<player>` — 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 `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
|
||||
.avahi.yml .dnsmasq.yml # optional DNS layers
|
||||
caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets (core/static/mirror)
|
||||
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)
|
||||
dnsmasq/dnsmasq.conf.tmpl # optional DNS config (rendered)
|
||||
docker/avahi/ # mDNS responder image
|
||||
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/
|
||||
tooling/ # build-stack, render-config, dns-records,
|
||||
# mirror-mods, mirror-airgap, fetch-launcher
|
||||
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.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Base Caddy config. Vhosts live in conf.d/*.caddy snippets that are mounted
|
||||
# in selectively so the stack can run with or without the static site / mirror:
|
||||
# conf.d/00-core.caddy auth + packwiz (always — core ingress)
|
||||
# conf.d/10-static.caddy apex landing + launcher downloads (static toggle)
|
||||
# conf.d/20-mirror.caddy air-gap upstream mirror (tls internal) (mirror toggle)
|
||||
# auto_https disable_redirects: http:// sites stay plaintext on :80; the mirror
|
||||
# snippet's hosts still get internal TLS on :443 when that snippet is present.
|
||||
# Base Caddy config. Vhosts live in conf.d/*.caddy snippets:
|
||||
# conf.d/00-core.caddy auth + packwiz
|
||||
# conf.d/10-static.caddy apex landing + launcher downloads
|
||||
# conf.d/30-distribution.caddy static site from another repo
|
||||
# conf.d/40-avatar.caddy nmsr skin/avatar renderer
|
||||
# conf.d/50-status.caddy uptime-kuma status page
|
||||
# All snippets are plain http:// on :80 — the host's nginx terminates TLS in
|
||||
# front (nginx/ulicraft-caddy.conf.tmpl). disable_redirects keeps caddy from
|
||||
# upgrading to its own https.
|
||||
{
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Core ingress — Blessing Skin variant. Replaces 00-core.caddy when the
|
||||
# docker-compose.blessingskin.yml override is in play (mounted at the same
|
||||
# target path). auth.* now points at Blessing Skin instead of Drasl.
|
||||
#
|
||||
# Blessing Skin serves BOTH its web UI (/) and the Yggdrasil API (/api/yggdrasil)
|
||||
# on the same vhost, so a single reverse_proxy covers everything.
|
||||
http://auth.{$BASE_DOMAIN} {
|
||||
reverse_proxy blessing-skin:80
|
||||
}
|
||||
|
||||
http://pack.{$BASE_DOMAIN} {
|
||||
root * /srv/pack
|
||||
file_server browse
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
# Static toggle — apex landing page + launcher downloads.
|
||||
# Mounted only when the stack runs with static files (build-stack.sh up static|full).
|
||||
# Apex — landing page (/srv/www) + launcher downloads (/srv/launcher).
|
||||
http://{$BASE_DOMAIN} {
|
||||
# Caddy's local CA root, so guests can trust the air-gap HTTPS mirror.
|
||||
# Mounted from ./caddy/caddy-root-ca.crt (exported by build-stack prep;
|
||||
# the live pki dir is root-only 0600 and can't be served directly).
|
||||
handle /ca.crt {
|
||||
root * /srv/ca-pub
|
||||
file_server
|
||||
}
|
||||
# Launcher downloads are a sibling mount (/srv/launcher), not nested under
|
||||
# the read-only /srv/www. handle_path strips the /launcher prefix.
|
||||
handle_path /launcher/* {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Mirror toggle — full client air-gap mirror (see plan/11-full-airgap-mirror.md).
|
||||
# Byte-exact copies of the real upstream hosts over HTTPS with Caddy's local CA.
|
||||
# dnsmasq spoofs these hostnames -> our host; {host} roots each at its subtree.
|
||||
# Mounted only when the stack runs with the mirror (build-stack.sh up mirror|full).
|
||||
meta.prismlauncher.org, piston-meta.mojang.com, piston-data.mojang.com, libraries.minecraft.net, maven.neoforged.net, resources.download.minecraft.net {
|
||||
tls internal
|
||||
root * /srv/mirror/{host}
|
||||
file_server
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
# Distribution toggle — static site served from a web root in ANOTHER repo.
|
||||
# The host path is set by DISTRIBUTION_WEB_ROOT in .env and bind-mounted to
|
||||
# /srv/distribution by docker-compose.caddy.yml. Mounted only when that var is
|
||||
# set (see the distribution override).
|
||||
# Distribution — static site served from a web root in ANOTHER repo. The host
|
||||
# path is set by DISTRIBUTION_WEB_ROOT in .env and bind-mounted to
|
||||
# /srv/distribution by the caddy service in docker-compose.yml.
|
||||
http://distribution.{$BASE_DOMAIN} {
|
||||
root * /srv/distribution
|
||||
file_server browse
|
||||
|
||||
4
caddy/conf.d/40-avatar.caddy
Normal file
4
caddy/conf.d/40-avatar.caddy
Normal file
@@ -0,0 +1,4 @@
|
||||
# Avatar — NMSR skin/avatar renderer (nmsr service), Drasl-backed.
|
||||
http://avatar.{$BASE_DOMAIN} {
|
||||
reverse_proxy nmsr:8080
|
||||
}
|
||||
4
caddy/conf.d/50-status.caddy
Normal file
4
caddy/conf.d/50-status.caddy
Normal file
@@ -0,0 +1,4 @@
|
||||
# Status — Uptime Kuma monitoring + public status page (uptime-kuma service).
|
||||
http://status.{$BASE_DOMAIN} {
|
||||
reverse_proxy uptime-kuma:3001
|
||||
}
|
||||
BIN
custom/BetterF3-11.0.3-NeoForge-1.21.1.jar
Normal file
BIN
custom/BetterF3-11.0.3-NeoForge-1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/BridgingMod-2.6.2+1.21.1.neoforge-release.jar
Normal file
BIN
custom/BridgingMod-2.6.2+1.21.1.neoforge-release.jar
Normal file
Binary file not shown.
BIN
custom/Controlling-neoforge-1.21.1-19.0.5.jar
Normal file
BIN
custom/Controlling-neoforge-1.21.1-19.0.5.jar
Normal file
Binary file not shown.
BIN
custom/ExplorersCompass-1.21.1-3.4.0-neoforge.jar
Normal file
BIN
custom/ExplorersCompass-1.21.1-3.4.0-neoforge.jar
Normal file
Binary file not shown.
BIN
custom/FarmersDelight-1.21.1-1.3.2.jar
Normal file
BIN
custom/FarmersDelight-1.21.1-1.3.2.jar
Normal file
Binary file not shown.
BIN
custom/FramedBlocks-10.6.0.jar
Normal file
BIN
custom/FramedBlocks-10.6.0.jar
Normal file
Binary file not shown.
BIN
custom/ImmediatelyFast-NeoForge-1.6.10+1.21.1.jar
Normal file
BIN
custom/ImmediatelyFast-NeoForge-1.6.10+1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/Necronomicon-NeoForge-1.6.0+1.21.jar
Normal file
BIN
custom/Necronomicon-NeoForge-1.6.0+1.21.jar
Normal file
Binary file not shown.
BIN
custom/Patchouli-1.21.1-93-NEOFORGE.jar
Normal file
BIN
custom/Patchouli-1.21.1-93-NEOFORGE.jar
Normal file
Binary file not shown.
BIN
custom/Searchables-neoforge-1.21.1-1.0.2.jar
Normal file
BIN
custom/Searchables-neoforge-1.21.1-1.0.2.jar
Normal file
Binary file not shown.
BIN
custom/TravelersTitles-1.21.1-NeoForge-5.1.3.jar
Normal file
BIN
custom/TravelersTitles-1.21.1-NeoForge-5.1.3.jar
Normal file
Binary file not shown.
BIN
custom/YungsApi-1.21.1-NeoForge-5.1.6.jar
Normal file
BIN
custom/YungsApi-1.21.1-NeoForge-5.1.6.jar
Normal file
Binary file not shown.
BIN
custom/appleskin-neoforge-mc1.21-3.0.9.jar
Normal file
BIN
custom/appleskin-neoforge-mc1.21-3.0.9.jar
Normal file
Binary file not shown.
BIN
custom/architectury-13.0.8-neoforge.jar
Normal file
BIN
custom/architectury-13.0.8-neoforge.jar
Normal file
Binary file not shown.
BIN
custom/baguettelib-1.21.1-NeoForge-2.0.3.jar
Normal file
BIN
custom/baguettelib-1.21.1-NeoForge-2.0.3.jar
Normal file
Binary file not shown.
BIN
custom/cardboardboxes-1.21.1-0.1.2.jar
Normal file
BIN
custom/cardboardboxes-1.21.1-0.1.2.jar
Normal file
Binary file not shown.
BIN
custom/cloth-config-15.0.140-neoforge.jar
Normal file
BIN
custom/cloth-config-15.0.140-neoforge.jar
Normal file
Binary file not shown.
BIN
custom/comforts-neoforge-9.0.5+1.21.1.jar
Normal file
BIN
custom/comforts-neoforge-9.0.5+1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/configured-neoforge-1.21.1-2.6.3.jar
Normal file
BIN
custom/configured-neoforge-1.21.1-2.6.3.jar
Normal file
Binary file not shown.
BIN
custom/corpse-neoforge-1.21.1-1.1.13.jar
Normal file
BIN
custom/corpse-neoforge-1.21.1-1.1.13.jar
Normal file
Binary file not shown.
BIN
custom/corpsecurioscompat-1.21.1-NeoForge-4.0.1.jar
Normal file
BIN
custom/corpsecurioscompat-1.21.1-NeoForge-4.0.1.jar
Normal file
Binary file not shown.
BIN
custom/curios-neoforge-9.5.1+1.21.1.jar
Normal file
BIN
custom/curios-neoforge-9.5.1+1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/drippyloadingscreen_neoforge_3.1.2_MC_1.21.1.jar
Normal file
BIN
custom/drippyloadingscreen_neoforge_3.1.2_MC_1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/entity_model_features-3.2.4-1.21-neoforge.jar
Normal file
BIN
custom/entity_model_features-3.2.4-1.21-neoforge.jar
Normal file
Binary file not shown.
BIN
custom/entity_texture_features_1.21-neoforge-7.1.jar
Normal file
BIN
custom/entity_texture_features_1.21-neoforge-7.1.jar
Normal file
Binary file not shown.
BIN
custom/entityculling-neoforge-1.10.2-mc1.21.1.jar
Normal file
BIN
custom/entityculling-neoforge-1.10.2-mc1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/fancymenu_neoforge_3.9.1_MC_1.21.1.jar
Normal file
BIN
custom/fancymenu_neoforge_3.9.1_MC_1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/fastleafdecay-35.jar
Normal file
BIN
custom/fastleafdecay-35.jar
Normal file
Binary file not shown.
BIN
custom/ferritecore-7.0.3-neoforge.jar
Normal file
BIN
custom/ferritecore-7.0.3-neoforge.jar
Normal file
Binary file not shown.
BIN
custom/ftb-chunks-neoforge-2101.1.17.jar
Normal file
BIN
custom/ftb-chunks-neoforge-2101.1.17.jar
Normal file
Binary file not shown.
BIN
custom/ftb-essentials-neoforge-2101.1.9.jar
Normal file
BIN
custom/ftb-essentials-neoforge-2101.1.9.jar
Normal file
Binary file not shown.
BIN
custom/ftb-library-neoforge-2101.1.31.jar
Normal file
BIN
custom/ftb-library-neoforge-2101.1.31.jar
Normal file
Binary file not shown.
BIN
custom/ftb-teams-neoforge-2101.1.10.jar
Normal file
BIN
custom/ftb-teams-neoforge-2101.1.10.jar
Normal file
Binary file not shown.
BIN
custom/ftb-ultimine-neoforge-2101.1.14.jar
Normal file
BIN
custom/ftb-ultimine-neoforge-2101.1.14.jar
Normal file
Binary file not shown.
BIN
custom/functionalstorage-1.21.1-1.5.7.jar
Normal file
BIN
custom/functionalstorage-1.21.1-1.5.7.jar
Normal file
Binary file not shown.
BIN
custom/handcrafted-neoforge-1.21.1-4.0.3.jar
Normal file
BIN
custom/handcrafted-neoforge-1.21.1-4.0.3.jar
Normal file
Binary file not shown.
BIN
custom/iris-neoforge-1.8.12+mc1.21.1.jar
Normal file
BIN
custom/iris-neoforge-1.8.12+mc1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/jamlib-neoforge-1.3.6+1.21.1.jar
Normal file
BIN
custom/jamlib-neoforge-1.3.6+1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/konkrete_neoforge_1.9.9_MC_1.21.jar
Normal file
BIN
custom/konkrete_neoforge_1.9.9_MC_1.21.jar
Normal file
Binary file not shown.
BIN
custom/kubejs-neoforge-2101.7.2-build.368.jar
Normal file
BIN
custom/kubejs-neoforge-2101.7.2-build.368.jar
Normal file
Binary file not shown.
BIN
custom/kubejsdelight-1.1.6.jar
Normal file
BIN
custom/kubejsdelight-1.1.6.jar
Normal file
Binary file not shown.
BIN
custom/lithium-neoforge-0.15.3+mc1.21.1.jar
Normal file
BIN
custom/lithium-neoforge-0.15.3+mc1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/lootr-neoforge-1.21.1-1.11.37.120.jar
Normal file
BIN
custom/lootr-neoforge-1.21.1-1.11.37.120.jar
Normal file
Binary file not shown.
BIN
custom/melody_neoforge_1.0.10_MC_1.21.jar
Normal file
BIN
custom/melody_neoforge_1.0.10_MC_1.21.jar
Normal file
Binary file not shown.
BIN
custom/morefunctionalstorage-1.2.3.jar
Normal file
BIN
custom/morefunctionalstorage-1.2.3.jar
Normal file
Binary file not shown.
BIN
custom/polymorph-neoforge-1.1.0+1.21.1.jar
Normal file
BIN
custom/polymorph-neoforge-1.1.0+1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/ponderjs-neoforge-1.21.1-2.4.0.jar
Normal file
BIN
custom/ponderjs-neoforge-1.21.1-2.4.0.jar
Normal file
Binary file not shown.
BIN
custom/resourcefullib-neoforge-1.21-3.0.12.jar
Normal file
BIN
custom/resourcefullib-neoforge-1.21-3.0.12.jar
Normal file
Binary file not shown.
BIN
custom/rhino-2101.2.7-build.85.jar
Normal file
BIN
custom/rhino-2101.2.7-build.85.jar
Normal file
Binary file not shown.
BIN
custom/rightclickharvest-neoforge-4.6.1+1.21.1.jar
Normal file
BIN
custom/rightclickharvest-neoforge-4.6.1+1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/simplemagnets-1.1.12c-neoforge-mc1.21.jar
Normal file
BIN
custom/simplemagnets-1.1.12c-neoforge-mc1.21.jar
Normal file
Binary file not shown.
BIN
custom/sodium-extra-neoforge-0.8.7+mc1.21.1.jar
Normal file
BIN
custom/sodium-extra-neoforge-0.8.7+mc1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar
Normal file
BIN
custom/sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/sophisticatedbackpacks-1.21.1-3.25.55.1852.jar
Normal file
BIN
custom/sophisticatedbackpacks-1.21.1-3.25.55.1852.jar
Normal file
Binary file not shown.
BIN
custom/sophisticatedcore-1.21.1-1.4.49.1976.jar
Normal file
BIN
custom/sophisticatedcore-1.21.1-1.4.49.1976.jar
Normal file
Binary file not shown.
BIN
custom/spark-1.10.124-neoforge.jar
Normal file
BIN
custom/spark-1.10.124-neoforge.jar
Normal file
Binary file not shown.
BIN
custom/supermartijn642configlib-1.1.8-neoforge-mc1.21.jar
Normal file
BIN
custom/supermartijn642configlib-1.1.8-neoforge-mc1.21.jar
Normal file
Binary file not shown.
BIN
custom/supermartijn642corelib-1.1.21-neoforge-mc1.21.jar
Normal file
BIN
custom/supermartijn642corelib-1.1.21-neoforge-mc1.21.jar
Normal file
Binary file not shown.
BIN
custom/tagtooltips-neoforge-1.21.1-1.2.0.jar
Normal file
BIN
custom/tagtooltips-neoforge-1.21.1-1.2.0.jar
Normal file
Binary file not shown.
BIN
custom/titanium-1.21-4.0.43.jar
Normal file
BIN
custom/titanium-1.21-4.0.43.jar
Normal file
Binary file not shown.
BIN
custom/torchmaster-neoforge-1.21.1-21.1.9.jar
Normal file
BIN
custom/torchmaster-neoforge-1.21.1-21.1.9.jar
Normal file
Binary file not shown.
BIN
custom/trashcans-1.0.18c-neoforge-mc1.21.jar
Normal file
BIN
custom/trashcans-1.0.18c-neoforge-mc1.21.jar
Normal file
Binary file not shown.
BIN
custom/utilitarian-1.21.1-0.19.2.jar
Normal file
BIN
custom/utilitarian-1.21.1-0.19.2.jar
Normal file
Binary file not shown.
BIN
custom/voicechat-neoforge-1.21.1-2.6.18.jar
Normal file
BIN
custom/voicechat-neoforge-1.21.1-2.6.18.jar
Normal file
Binary file not shown.
BIN
custom/welcomescreen-neoforge-1.0.0-1.21.1.jar
Normal file
BIN
custom/welcomescreen-neoforge-1.0.0-1.21.1.jar
Normal file
Binary file not shown.
BIN
custom/yeetusexperimentus-neoforge-87.0.0.jar
Normal file
BIN
custom/yeetusexperimentus-neoforge-87.0.0.jar
Normal file
Binary file not shown.
BIN
custom/yet_another_config_lib_v3-3.8.2+1.21.1-neoforge.jar
Normal file
BIN
custom/yet_another_config_lib_v3-3.8.2+1.21.1-neoforge.jar
Normal file
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
# Resolve every *.${BASE_DOMAIN} (and the apex) to the Docker host LAN IP.
|
||||
# Rendered by tooling/render-config.sh -> dnsmasq/dnsmasq.conf (gitignored).
|
||||
address=/${BASE_DOMAIN}/${HOST_LAN_IP}
|
||||
no-resolv
|
||||
|
||||
# Minecraft SRV record: guests connect to "mc.${BASE_DOMAIN}" with NO port —
|
||||
# the SRV lookup points the client at port 25565. Target mc.${BASE_DOMAIN}
|
||||
# resolves to HOST_LAN_IP via the wildcard above.
|
||||
# Format: srv-host=_service._proto.name,target,port
|
||||
srv-host=_minecraft._tcp.mc.${BASE_DOMAIN},mc.${BASE_DOMAIN},25565
|
||||
|
||||
# Full client air-gap (see plan/11-full-airgap-mirror.md): spoof the real
|
||||
# upstream hosts -> our Caddy mirror. Auth hosts (session/textures/api.mojang)
|
||||
# are NOT spoofed — drasl handles those via authlib-injector. Keep this list in
|
||||
# sync with the proxy-capture step.
|
||||
address=/meta.prismlauncher.org/${HOST_LAN_IP}
|
||||
address=/piston-meta.mojang.com/${HOST_LAN_IP}
|
||||
address=/piston-data.mojang.com/${HOST_LAN_IP}
|
||||
address=/libraries.minecraft.net/${HOST_LAN_IP}
|
||||
address=/maven.neoforged.net/${HOST_LAN_IP}
|
||||
address=/resources.download.minecraft.net/${HOST_LAN_IP}
|
||||
@@ -1,28 +0,0 @@
|
||||
# mDNS toggle — publish *.local service names via the HOST's avahi-daemon for
|
||||
# zero-config guest resolution. Enabled when ENABLE_MDNS=true in .env and only
|
||||
# meaningful when BASE_DOMAIN=*.local.
|
||||
#
|
||||
# Host prerequisites:
|
||||
# - avahi-daemon running on the host
|
||||
# - if the host has many interfaces (e.g. lots of docker bridges), restrict
|
||||
# avahi to the LAN NIC in /etc/avahi/avahi-daemon.conf:
|
||||
# allow-interfaces=<lan-iface>
|
||||
# otherwise avahi sees its own announcements across bridges → "Local name
|
||||
# collision" and publishing fails.
|
||||
services:
|
||||
avahi:
|
||||
build: ./docker/avahi
|
||||
image: ulicraft-avahi:local
|
||||
container_name: avahi
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
# AppArmor's docker-default profile blocks the D-Bus publish to host avahi.
|
||||
security_opt:
|
||||
- apparmor=unconfined
|
||||
environment:
|
||||
BASE_DOMAIN: ${BASE_DOMAIN}
|
||||
HOST_LAN_IP: ${HOST_LAN_IP}
|
||||
DBUS_SYSTEM_BUS_ADDRESS: "unix:path=/run/dbus/system_bus_socket"
|
||||
volumes:
|
||||
# Talk to the host's avahi-daemon (host must run avahi-daemon).
|
||||
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket
|
||||
@@ -1,105 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Blessing Skin auth variant — use INSTEAD of Drasl.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Layer this over the base file AND the caddy ingress (caddy lives in its own
|
||||
# file now; this override only swaps caddy's core conf, so caddy.yml is required):
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
||||
# -f docker-compose.blessingskin.yml up -d
|
||||
#
|
||||
# What it does vs the base stack:
|
||||
# - drasl → left running but UNUSED + unreachable (Caddy no longer
|
||||
# routes to it; it has no host port). Compose merges
|
||||
# depends_on, so it can't be removed from an override —
|
||||
# idling it is the clean option. `docker compose ... stop
|
||||
# drasl` after up if you want it down.
|
||||
# - caddy → auth.* now reverse-proxies blessing-skin:80
|
||||
# - mariadb → new; Blessing Skin's datastore (no SQLite support)
|
||||
# - blessing-skin → new; PHP skin server + yggdrasil-api plugin
|
||||
# - minecraft → authlib-injector now points at /api/yggdrasil,
|
||||
# ONLINE_MODE=TRUE (real session validation against BSS)
|
||||
#
|
||||
# First-run is a WEB WIZARD — see plan/03b-blessing-skin.md. The yggdrasil-api
|
||||
# plugin must be installed + enabled from the BSS admin panel before the
|
||||
# /api/yggdrasil endpoint (and thus Minecraft login) works.
|
||||
#
|
||||
# DB creds + APP_KEY come from .env (see .env.example: BS_* vars).
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
services:
|
||||
caddy:
|
||||
# Swap the core ingress conf for the Blessing Skin one. Compose MERGES
|
||||
# volumes by container path, so mounting the BSS conf at the SAME target
|
||||
# (/etc/caddy/conf.d/00-core.caddy) shadows the base 00-core.caddy; all
|
||||
# other base mounts (pack, custom, static, ca) are kept automatically.
|
||||
volumes:
|
||||
- ./caddy/conf.d/00-core-blessingskin.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
||||
depends_on:
|
||||
- blessing-skin
|
||||
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
container_name: mariadb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MARIADB_DATABASE: ${BS_DB_NAME}
|
||||
MARIADB_USER: ${BS_DB_USER}
|
||||
MARIADB_PASSWORD: ${BS_DB_PASSWORD}
|
||||
MARIADB_ROOT_PASSWORD: ${BS_DB_ROOT_PASSWORD}
|
||||
TZ: "Europe/Madrid"
|
||||
volumes:
|
||||
- bs_db:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
blessing-skin:
|
||||
image: azusamikan/blessing-skin-server-docker:latest
|
||||
container_name: blessing-skin
|
||||
restart: unless-stopped
|
||||
# Internal-only — reached via Caddy at auth.${BASE_DOMAIN}.
|
||||
environment:
|
||||
APP_URL: "http://auth.${BASE_DOMAIN}"
|
||||
# Persist APP_KEY across recreates (else sessions/encryption break).
|
||||
# Generate once: docker run --rm azusamikan/blessing-skin-server-docker:latest php artisan key:generate --show
|
||||
APP_KEY: ${BS_APP_KEY}
|
||||
DB_DRIVER: "mysql"
|
||||
DB_HOST: "mariadb"
|
||||
DB_PORT: "3306"
|
||||
DB_DATABASE: ${BS_DB_NAME}
|
||||
DB_USERNAME: ${BS_DB_USER}
|
||||
DB_PASSWORD: ${BS_DB_PASSWORD}
|
||||
TZ: "Europe/Madrid"
|
||||
volumes:
|
||||
- bs_storage:/app/storage # uploaded skins/capes + textures
|
||||
- bs_plugins:/app/plugins # yggdrasil-api plugin lives here
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
minecraft:
|
||||
environment:
|
||||
# authlib-injector API root for Blessing Skin's yggdrasil-api plugin
|
||||
# is <site>/api/yggdrasil (NOT /authlib-injector like Drasl).
|
||||
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=http://auth.${BASE_DOMAIN}/api/yggdrasil"
|
||||
# authlib-injector needs real auth: online-mode TRUE so the server
|
||||
# validates sessions against Blessing Skin. (Drasl variant used FALSE.)
|
||||
ONLINE_MODE: "TRUE"
|
||||
# Keep FALSE for safety on 1.21+. The yggdrasil-api plugin DOES sign
|
||||
# profile keys, so you MAY flip this to TRUE if signed chat is wanted.
|
||||
ENFORCE_SECURE_PROFILE: "FALSE"
|
||||
# Merged with the base depends_on (drasl, caddy); just adds blessing-skin so
|
||||
# Minecraft starts after the skin server is up.
|
||||
depends_on:
|
||||
- blessing-skin
|
||||
|
||||
volumes:
|
||||
bs_db:
|
||||
bs_storage:
|
||||
bs_plugins:
|
||||
@@ -1,50 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Caddy ingress — LAN / air-gap path. NOT started with the base stack.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Caddy was moved out of docker-compose.yml so the base stack can run behind
|
||||
# the host's nginx (production) without it. build-stack.sh adds this file for
|
||||
# its online/airgap/core modes; static/mirror overrides mount extra vhosts in.
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||
#
|
||||
# Holds the mcnet aliases (auth./pack.${BASE_DOMAIN}) so containers resolve the
|
||||
# stack's own names internally — this is why minecraft can reach the pack here
|
||||
# without extra_hosts (the nginx path uses extra_hosts instead).
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
# Host-published port. Default 80 for the standalone LAN/air-gap path; set
|
||||
# CADDY_HTTP_PORT to a high localhost-only port when the machine's nginx
|
||||
# fronts caddy (nginx terminates TLS, reverse-proxies here by Host header —
|
||||
# see nginx/ulicraft-caddy.conf.tmpl).
|
||||
ports:
|
||||
- "${CADDY_HTTP_BIND:-0.0.0.0}:${CADDY_HTTP_PORT:-80}:80"
|
||||
environment:
|
||||
BASE_DOMAIN: ${BASE_DOMAIN}
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./caddy/conf.d/00-core.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
||||
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
|
||||
- ./www:/srv/www:ro
|
||||
- ./mirror/launcher:/srv/launcher:ro
|
||||
- ./caddy/caddy-root-ca.crt:/srv/ca-pub/ca.crt:ro
|
||||
- ./pack:/srv/pack:ro
|
||||
# Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh
|
||||
# doesn't index them as direct files. Served at pack.${BASE_DOMAIN}/custom/.
|
||||
- ./custom:/srv/pack/custom:ro
|
||||
networks:
|
||||
mcnet:
|
||||
# The upstream spoof-host aliases live in docker-compose.mirror.yml —
|
||||
# adding them here would hijack maven.neoforged.net etc. for ALL mcnet
|
||||
# containers, breaking the ONLINE NeoForge install during pre-bake.
|
||||
aliases:
|
||||
- "auth.${BASE_DOMAIN}"
|
||||
- "pack.${BASE_DOMAIN}"
|
||||
|
||||
# Restore the ordering the base file used to have (base drops it so it can run
|
||||
# caddy-less behind nginx).
|
||||
minecraft:
|
||||
depends_on:
|
||||
- caddy
|
||||
@@ -1,15 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Distribution vhost — distribution.${BASE_DOMAIN}, static files from a web
|
||||
# root that lives in ANOTHER repo. Layered on top of the caddy ingress.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
||||
# -f docker-compose.distribution.yml up -d
|
||||
#
|
||||
# DISTRIBUTION_WEB_ROOT (.env) = absolute host path to the built static site in
|
||||
# the other repo. Bind-mounted read-only; caddy serves it via the
|
||||
# conf.d/30-distribution.caddy snippet.
|
||||
services:
|
||||
caddy:
|
||||
volumes:
|
||||
- ./caddy/conf.d/30-distribution.caddy:/etc/caddy/conf.d/30-distribution.caddy:ro
|
||||
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}:/srv/distribution:ro
|
||||
@@ -1,20 +0,0 @@
|
||||
# Optional turnkey DNS — only if you DON'T run your own party DNS server.
|
||||
# Layer on: `docker compose -f docker-compose.yml -f docker-compose.dnsmasq.yml ... up -d`.
|
||||
# Serves dnsmasq/dnsmasq.conf (rendered from the template, includes the airgap
|
||||
# spoof records). If you have your own DNS, skip this and use the records from
|
||||
# `tooling/dns-records.sh` instead.
|
||||
services:
|
||||
dnsmasq:
|
||||
image: jpillora/dnsmasq
|
||||
container_name: dnsmasq
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Bind DNS to the host LAN IP so it doesn't clash with systemd-resolved on :53.
|
||||
- "${HOST_LAN_IP}:53:53/udp"
|
||||
- "${HOST_LAN_IP}:53:53/tcp"
|
||||
volumes:
|
||||
- ./dnsmasq/dnsmasq.conf:/etc/dnsmasq.conf:ro
|
||||
networks:
|
||||
- mcnet
|
||||
# webproc UI (:8080) can be exposed behind Caddy as dns.${BASE_DOMAIN};
|
||||
# guard with HTTP_USER / HTTP_PASS.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Mirror toggle — full client air-gap upstream mirror (see plan/11).
|
||||
# Layer on top of the base: `docker compose -f docker-compose.yml -f docker-compose.mirror.yml up -d`
|
||||
# (or `tooling/build-stack.sh --up mirror`). Adds the mirror vhost snippet, the
|
||||
# upstream content mount, and publishes :443 for Caddy's `tls internal` certs.
|
||||
# Populate the mirror first: tooling/mirror-airgap.sh (+ the proxy-capture step).
|
||||
services:
|
||||
caddy:
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./caddy/conf.d/20-mirror.caddy:/etc/caddy/conf.d/20-mirror.caddy:ro
|
||||
- ./mirror/upstream:/srv/mirror:ro
|
||||
networks:
|
||||
mcnet:
|
||||
# Spoof the real upstream hosts -> Caddy (air-gap runtime only). Includes
|
||||
# the base aliases because compose replaces (not merges) the alias list.
|
||||
aliases:
|
||||
- "auth.${BASE_DOMAIN}"
|
||||
- "packwiz.${BASE_DOMAIN}"
|
||||
- "meta.prismlauncher.org"
|
||||
- "piston-meta.mojang.com"
|
||||
- "piston-data.mojang.com"
|
||||
- "libraries.minecraft.net"
|
||||
- "maven.neoforged.net"
|
||||
- "resources.download.minecraft.net"
|
||||
@@ -1,30 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Production ingress = the HOST's nginx + Let's Encrypt. No caddy.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
#
|
||||
# nginx (on the host, already installed) terminates TLS with the LE certs from
|
||||
# tooling/issue-letsencrypt.sh and:
|
||||
# - serves apex + pack.${BASE_DOMAIN} as static files (./www, ./pack, ./custom)
|
||||
# - reverse-proxies auth.${BASE_DOMAIN} -> 127.0.0.1:25585 (drasl)
|
||||
# See nginx/ulicraft.conf.tmpl.
|
||||
#
|
||||
# This override:
|
||||
# - publishes drasl on localhost so the host nginx can reach it
|
||||
# - points the minecraft container at the host (extra_hosts) over HTTPS, so it
|
||||
# fetches the pack and validates sessions through the same public names the
|
||||
# LE certs cover (JVM trusts Let's Encrypt out of the box)
|
||||
services:
|
||||
drasl:
|
||||
ports:
|
||||
- "127.0.0.1:25585:25585"
|
||||
|
||||
minecraft:
|
||||
# Make the public names resolve to the host running nginx.
|
||||
extra_hosts:
|
||||
- "auth.${BASE_DOMAIN}:host-gateway"
|
||||
- "pack.${BASE_DOMAIN}:host-gateway"
|
||||
environment:
|
||||
# Use HTTPS now that nginx terminates TLS with a publicly-trusted cert.
|
||||
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=https://auth.${BASE_DOMAIN}/authlib-injector"
|
||||
PACKWIZ_URL: "https://pack.${BASE_DOMAIN}/pack.toml"
|
||||
@@ -1,11 +0,0 @@
|
||||
# Static toggle — apex landing page + launcher downloads.
|
||||
# Layer on top of the base: `docker compose -f docker-compose.yml -f docker-compose.static.yml up -d`
|
||||
# (or `tooling/build-stack.sh --up static`). Adds the static vhost snippet plus
|
||||
# the www + launcher content mounts. Build www first: cd landing && pnpm run build.
|
||||
services:
|
||||
caddy:
|
||||
volumes:
|
||||
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
|
||||
- ./www:/srv/www:ro
|
||||
- ./mirror/launcher:/srv/launcher:ro
|
||||
- ./caddy/caddy-root-ca.crt:/srv/ca-pub/ca.crt:ro
|
||||
@@ -1,31 +1,31 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Ulicraft LAN-party stack — Minecraft 1.21.1 NeoForge + Drasl auth
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Single source of config: BASE_DOMAIN + HOST_LAN_IP in .env.
|
||||
# DNS is provided by YOUR party DNS server — see tooling/dns-records.sh for the
|
||||
# records to add (online vs airgap). A turnkey dnsmasq is available as an
|
||||
# optional layer: docker-compose.dnsmasq.yml.
|
||||
# - ingress is layered on, NOT in this base file: caddy (docker-compose.caddy.yml,
|
||||
# LAN/air-gap, holds mcnet aliases) OR the host's nginx (docker-compose.nginx.yml
|
||||
# + Let's Encrypt, production)
|
||||
# - drasl handles auth (password login; NO Keycloak/OIDC in this stack)
|
||||
# - packwiz pack is served by the ingress at pack.${BASE_DOMAIN}
|
||||
# - minecraft installs mods via PACKWIZ_URL, authlib-injector -> auth.
|
||||
# Single, unified compose. One file holds the whole stack:
|
||||
# drasl auth + skins (Yggdrasil), internal only
|
||||
# minecraft the server (itzg/NEOFORGE), installs mods via packwiz
|
||||
# mc-backup world backups every 6h via RCON
|
||||
# caddy internal ingress — routes auth./pack./apex/launcher/avatar.
|
||||
# /distribution. by Host header (conf.d/*.caddy)
|
||||
# nmsr skin/avatar renderer at avatar.${BASE_DOMAIN}
|
||||
# uptime-kuma status monitoring at status.${BASE_DOMAIN}
|
||||
#
|
||||
# Bring up: docker compose up -d (render configs first: tooling/render-config.sh)
|
||||
#
|
||||
# DNS is handled OUTSIDE this repo — point every service name at the host.
|
||||
# Production TLS terminates at the HOST's nginx in front of caddy
|
||||
# (nginx/ulicraft-caddy.conf.tmpl + Let's Encrypt); caddy is published on a
|
||||
# localhost-only port (CADDY_HTTP_BIND/CADDY_HTTP_PORT in .env). Containers reach
|
||||
# the stack's own names internally via caddy's mcnet aliases (auth./pack.).
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
services:
|
||||
# NOTE: the HTTP(S) ingress is NOT in this base file.
|
||||
# - LAN / air-gap: caddy (docker-compose.caddy.yml) — added by build-stack.sh
|
||||
# - production: the HOST's nginx + Let's Encrypt (docker-compose.nginx.yml
|
||||
# publishes drasl to localhost; nginx/ulicraft.conf.tmpl)
|
||||
# Base alone (drasl + minecraft + mc-backup) is not a complete deployment —
|
||||
# always layer one ingress file on top.
|
||||
drasl:
|
||||
image: unmojang/drasl:latest
|
||||
container_name: drasl
|
||||
restart: unless-stopped
|
||||
# Internal-only: no published host port. Reached via Caddy at
|
||||
# auth.${BASE_DOMAIN} (Caddy holds the network alias on mcnet).
|
||||
# Internal-only: no published host port. Reached via caddy at
|
||||
# auth.${BASE_DOMAIN} (caddy holds the network alias on mcnet).
|
||||
volumes:
|
||||
- ./drasl/config:/etc/drasl:ro
|
||||
- drasl_state:/var/lib/drasl
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
|
||||
# ── Auth: offline mode + authlib-injector pointing at Drasl ─
|
||||
ONLINE_MODE: "FALSE"
|
||||
# mount authlib-injector.jar at /extras/authlib-injector.jar
|
||||
# Resolves over mcnet to caddy (alias auth.${BASE_DOMAIN}) -> drasl.
|
||||
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=http://auth.${BASE_DOMAIN}/authlib-injector"
|
||||
|
||||
# ── Server config ───────────────────────────────────────────
|
||||
@@ -71,9 +71,7 @@ services:
|
||||
ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+
|
||||
ALLOW_FLIGHT: "TRUE" # many tech mods need this
|
||||
|
||||
# ── Mod source: packwiz pack served by the ingress ──────────
|
||||
# http + caddy alias by default; the nginx override switches this to
|
||||
# https + host-gateway (extra_hosts) for the production path.
|
||||
# ── Mod source: packwiz pack served by caddy (alias pack.) ──
|
||||
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
|
||||
|
||||
# ── RCON for remote admin ───────────────────────────────────
|
||||
@@ -86,11 +84,17 @@ services:
|
||||
- mc_data:/data
|
||||
- ./runtime:/extras:ro # authlib-injector.jar lives here
|
||||
depends_on:
|
||||
- drasl # ingress (caddy/nginx) is layered separately
|
||||
- drasl
|
||||
- caddy # pack./auth. must resolve at boot
|
||||
# Containers don't inherit the host's /etc/hosts; the LAN resolver returns a
|
||||
# dead address for the public names. Pin auth./pack. to the host so HTTPS to
|
||||
# them lands on the host's nginx (valid LE cert) -> caddy -> service.
|
||||
extra_hosts:
|
||||
- "auth.${BASE_DOMAIN}:host-gateway"
|
||||
- "pack.${BASE_DOMAIN}:host-gateway"
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
# Optional: backups
|
||||
mc-backup:
|
||||
image: itzg/mc-backup
|
||||
container_name: mc-backup
|
||||
@@ -109,9 +113,72 @@ services:
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
# Internal ingress. Routes every vhost by Host header (conf.d/*.caddy):
|
||||
# auth. -> drasl, pack. -> packwiz files, apex -> landing + /launcher,
|
||||
# avatar. -> nmsr, status. -> uptime-kuma, distribution. -> static site.
|
||||
# Published on a localhost-only port; the host's nginx terminates TLS in
|
||||
# front of it (nginx/ulicraft-caddy.conf.tmpl).
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${CADDY_HTTP_BIND:-127.0.0.1}:${CADDY_HTTP_PORT:-8880}:80"
|
||||
environment:
|
||||
BASE_DOMAIN: ${BASE_DOMAIN}
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./caddy/conf.d/00-core.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
||||
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
|
||||
- ./caddy/conf.d/30-distribution.caddy:/etc/caddy/conf.d/30-distribution.caddy:ro
|
||||
- ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro
|
||||
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
|
||||
- ./www:/srv/www:ro
|
||||
- ./launcher:/srv/launcher:ro
|
||||
- ./pack:/srv/pack:ro
|
||||
# Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh
|
||||
# doesn't index them as direct files. Served at pack.${BASE_DOMAIN}/custom/.
|
||||
# Nested under the :ro /srv/pack mount — the mountpoint pack/custom/ must
|
||||
# pre-exist on the host (tracked via pack/custom/.gitkeep) or Docker can't
|
||||
# mkdir it in the read-only parent.
|
||||
- ./custom:/srv/pack/custom:ro
|
||||
# Static site from ANOTHER repo (absolute host path in .env).
|
||||
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}:/srv/distribution:ro
|
||||
networks:
|
||||
mcnet:
|
||||
# Containers resolve the stack's own public names to caddy internally.
|
||||
aliases:
|
||||
- "auth.${BASE_DOMAIN}"
|
||||
- "pack.${BASE_DOMAIN}"
|
||||
|
||||
# Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls
|
||||
# player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080.
|
||||
nmsr:
|
||||
build:
|
||||
context: ./docker/nmsr
|
||||
image: ulicraft/nmsr:local
|
||||
container_name: nmsr
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nmsr/config.toml:/nmsr/config.toml:ro
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
# Status monitoring + public status page. caddy proxies status. -> :3001.
|
||||
# Joins mcnet so monitors probe the stack by internal service name.
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:1
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- kuma_data:/app/data
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
volumes:
|
||||
drasl_state:
|
||||
mc_data:
|
||||
kuma_data:
|
||||
|
||||
networks:
|
||||
mcnet:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Publishes the stack's service names on .local via the HOST's avahi-daemon
|
||||
# (over its D-Bus socket — no second daemon, avoids the :5353 collision).
|
||||
# Requires the host to run avahi-daemon and the socket to be mounted (see
|
||||
# docker-compose.avahi.yml). Used when ENABLE_MDNS=true and BASE_DOMAIN=*.local.
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache avahi-tools
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Publish A records for the stack's service names on .local via the HOST's
|
||||
# avahi-daemon (reached over its mounted D-Bus socket). One avahi-publish per
|
||||
# name; if any drops the container exits so Docker restarts it.
|
||||
set -eu
|
||||
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset}"
|
||||
: "${HOST_LAN_IP:?HOST_LAN_IP unset}"
|
||||
|
||||
case "$BASE_DOMAIN" in
|
||||
*.local) ;;
|
||||
*) echo "WARN: mDNS resolves only .local — BASE_DOMAIN=$BASE_DOMAIN will NOT be discoverable via avahi. Set BASE_DOMAIN=*.local to use mDNS." >&2 ;;
|
||||
esac
|
||||
|
||||
bus="${DBUS_SYSTEM_BUS_ADDRESS:-unix:path=/run/dbus/system_bus_socket}"
|
||||
sock=${bus#unix:path=}
|
||||
if [ ! -S "$sock" ]; then
|
||||
echo "ERROR: host D-Bus socket '$sock' not found. Mount the host's" >&2
|
||||
echo " /run/dbus/system_bus_socket and ensure avahi-daemon runs on the host." >&2
|
||||
exit 1
|
||||
fi
|
||||
export DBUS_SYSTEM_BUS_ADDRESS="$bus"
|
||||
|
||||
pids=""
|
||||
for n in "$BASE_DOMAIN" "auth.$BASE_DOMAIN" "packwiz.$BASE_DOMAIN" "mc.$BASE_DOMAIN"; do
|
||||
echo "mDNS publish (host avahi): $n -> $HOST_LAN_IP"
|
||||
avahi-publish -a "$n" "$HOST_LAN_IP" &
|
||||
pids="$pids $!"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
wait -n $pids 2>/dev/null || wait
|
||||
33
docker/nmsr/Dockerfile
Normal file
33
docker/nmsr/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# NMSR-as-a-Service (https://github.com/NickAcPT/nmsr-rs) — built from source.
|
||||
# Upstream ships no published image, so we vendor a pinned build. Adapted from
|
||||
# the upstream Dockerfile but: pinned to a commit (reproducible), and the config
|
||||
# is bind-mounted at runtime (not COPYed) so it can change without a rebuild.
|
||||
#
|
||||
# Pin — bump deliberately:
|
||||
ARG NMSR_REF=948ba4bc027b
|
||||
|
||||
FROM rust:slim-bookworm AS builder
|
||||
ARG NMSR_REF
|
||||
WORKDIR /tmp
|
||||
RUN apt-get update -y \
|
||||
&& apt-get --no-install-recommends install -y git libssl-dev pkg-config ca-certificates
|
||||
RUN git clone https://github.com/NickAcPT/nmsr-rs/
|
||||
WORKDIR /tmp/nmsr-rs
|
||||
RUN git checkout "${NMSR_REF}"
|
||||
# target-cpu=native: this image is built on the host that runs it (self-host).
|
||||
RUN RUSTFLAGS="-Ctarget-cpu=native" \
|
||||
cargo build --release --bin nmsr-aas --features ears --package nmsr-aas
|
||||
|
||||
FROM rust:slim-bookworm
|
||||
# mesa-vulkan-drivers provides lavapipe (software Vulkan) for headless rendering.
|
||||
RUN apt-get update -y \
|
||||
&& apt-get --no-install-recommends install -y mesa-vulkan-drivers ca-certificates
|
||||
WORKDIR /nmsr
|
||||
COPY --from=builder /tmp/nmsr-rs/target/release/nmsr-aas /nmsr/nmsr-aas
|
||||
ENV NMSR_USE_SMAA=1 \
|
||||
NMSR_SAMPLE_COUNT=1 \
|
||||
WGPU_BACKEND=vulkan \
|
||||
RUST_BACKTRACE=1
|
||||
EXPOSE 8080
|
||||
# config.toml is bind-mounted by docker-compose.nmsr.yml.
|
||||
CMD ["/nmsr/nmsr-aas", "-c", "/nmsr/config.toml"]
|
||||
@@ -3,7 +3,10 @@
|
||||
# -> drasl/config/config.toml (gitignored). Only ${BASE_DOMAIN} is substituted.
|
||||
# Reached only via Caddy at auth.${BASE_DOMAIN}; drasl has no published host port.
|
||||
|
||||
BaseURL = "http://auth.${BASE_DOMAIN}"
|
||||
# Public scheme is HTTPS — the host nginx terminates TLS in front of caddy.
|
||||
# Drasl builds absolute URLs (textures, authlib-injector API location) from this;
|
||||
# an http:// value triggers mixed-content + broken login on the https origin.
|
||||
BaseURL = "https://auth.${BASE_DOMAIN}"
|
||||
Domain = "auth.${BASE_DOMAIN}"
|
||||
ListenAddress = "0.0.0.0:25585" # internal; Caddy reverse-proxies to it
|
||||
DefaultAdminUsernames = ["admin"]
|
||||
|
||||
@@ -24,19 +24,15 @@ export const site = {
|
||||
dust: true,
|
||||
},
|
||||
|
||||
// Connect target shown to guests. No port needed — a dnsmasq SRV record
|
||||
// (_minecraft._tcp.mc.<domain>) points clients at 25565.
|
||||
serverAddress: `mc.${BASE_DOMAIN}`,
|
||||
// Connect target shown to guests. No port needed (defaults to 25565).
|
||||
serverAddress: `${BASE_DOMAIN}`,
|
||||
|
||||
// Drasl auth web UI + authlib-injector API root.
|
||||
authUrl: `http://auth.${BASE_DOMAIN}`,
|
||||
authlibUrl: `http://auth.${BASE_DOMAIN}/authlib-injector`,
|
||||
// Drasl auth web UI + authlib-injector API root (public HTTPS).
|
||||
authUrl: `https://auth.${BASE_DOMAIN}`,
|
||||
authlibUrl: `https://auth.${BASE_DOMAIN}/authlib-injector`,
|
||||
|
||||
// packwiz modpack entrypoint.
|
||||
packwizUrl: `http://packwiz.${BASE_DOMAIN}/pack.toml`,
|
||||
|
||||
// Caddy local CA root — guests import this to trust the offline asset mirror.
|
||||
caCertUrl: `http://${BASE_DOMAIN}/ca.crt`,
|
||||
// packwiz modpack entrypoint (public HTTPS, pack. subdomain).
|
||||
packwizUrl: `https://pack.${BASE_DOMAIN}/pack.toml`,
|
||||
|
||||
// Static server-list panel (Status section). No fake live count — see plan
|
||||
// 09-landing.md option B.
|
||||
|
||||
@@ -54,9 +54,6 @@ export interface UI {
|
||||
pb: string; // after it
|
||||
copy: string;
|
||||
registerPre: string;
|
||||
caPre: string;
|
||||
caLink: string;
|
||||
caPost: string;
|
||||
};
|
||||
s3: { kicker: string; h: string; p: string };
|
||||
s4: {
|
||||
@@ -137,9 +134,6 @@ export const ui: Record<Lang, UI> = {
|
||||
pb: "account using this URL, then log in with your Ulicraft credentials.",
|
||||
copy: "Copy",
|
||||
registerPre: "Need credentials? Register at",
|
||||
caPre: "Offline party? Download and trust the",
|
||||
caLink: "local CA certificate",
|
||||
caPost: "so the game-file mirror works over HTTPS.",
|
||||
},
|
||||
s3: {
|
||||
kicker: "Modpack",
|
||||
@@ -226,9 +220,6 @@ export const ui: Record<Lang, UI> = {
|
||||
pb: "con esta URL y luego inicia sesión con tus credenciales de Ulicraft.",
|
||||
copy: "Copiar",
|
||||
registerPre: "¿Sin credenciales? Regístrate en",
|
||||
caPre: "¿Fiesta sin internet? Descarga y confía en el",
|
||||
caLink: "certificado CA local",
|
||||
caPost: "para que el mirror de archivos funcione por HTTPS.",
|
||||
},
|
||||
s3: {
|
||||
kicker: "Modpack",
|
||||
@@ -315,9 +306,6 @@ export const ui: Record<Lang, UI> = {
|
||||
pb: "kontu bat URL honekin, eta gero hasi saioa zure Ulicraft kredentzialekin.",
|
||||
copy: "Kopiatu",
|
||||
registerPre: "Kredentzialik ez? Erregistratu hemen:",
|
||||
caPre: "Internetik gabeko festa? Deskargatu eta fidatu",
|
||||
caLink: "tokiko CA ziurtagiriaz",
|
||||
caPost: "fitxategi-mirrorrak HTTPS bidez ibil dadin.",
|
||||
},
|
||||
s3: {
|
||||
kicker: "Modpacka",
|
||||
|
||||
@@ -207,9 +207,6 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
<p>
|
||||
{t.join.s2.registerPre} <a href={site.authUrl}>{site.authUrl}</a>.
|
||||
</p>
|
||||
<span class="hint">
|
||||
{t.join.s2.caPre} <a href={site.caCertUrl}>{t.join.s2.caLink}</a> {t.join.s2.caPost}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Ulicraft host nginx — TLS terminator in FRONT of caddy.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Alternative to ulicraft.conf.tmpl. Instead of nginx serving the static
|
||||
# files + proxying only drasl, here caddy (the docker container) is the single
|
||||
# ingress for ALL vhosts and nginx just terminates TLS and reverse-proxies
|
||||
# everything to caddy by Host header.
|
||||
# caddy (in docker-compose.yml) is the single internal ingress for ALL vhosts;
|
||||
# this host nginx terminates TLS and reverse-proxies everything to caddy by Host
|
||||
# header. Prefer tooling/render-nginx.sh over hand-rendering.
|
||||
#
|
||||
# Caddy must be published on a localhost port — set in .env:
|
||||
# CADDY_HTTP_PORT=8880
|
||||
# CADDY_HTTP_BIND=127.0.0.1
|
||||
# and the stack brought up with the caddy override (NOT the nginx override):
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
||||
# -f docker-compose.static.yml up -d
|
||||
# and the stack brought up:
|
||||
# docker compose up -d
|
||||
#
|
||||
# Render with BOTH vars then install:
|
||||
# Render with the vars then install:
|
||||
# CADDY_HTTP_PORT=8880 BASE_DOMAIN=ulicraft.net APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 \
|
||||
# envsubst '$CADDY_HTTP_PORT $BASE_DOMAIN $APP_DIR' \
|
||||
# < nginx/ulicraft-caddy.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
|
||||
@@ -32,7 +30,7 @@ upstream ulicraft_caddy {
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${BASE_DOMAIN} auth.${BASE_DOMAIN} pack.${BASE_DOMAIN} distribution.${BASE_DOMAIN};
|
||||
server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} pack.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
@@ -40,13 +38,13 @@ server {
|
||||
# caddy routes by the forwarded Host header (apex -> www, auth -> drasl,
|
||||
# pack -> packwiz files).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name ${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
client_max_body_size 4m; # skin uploads (auth) reuse the same proxy block
|
||||
|
||||
@@ -59,14 +57,27 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# www -> apex canonical redirect (needs its own cert; covered by LE_SUBDOMAINS).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name www.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/www.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/www.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
return 301 https://${BASE_DOMAIN}$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name auth.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/auth.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
client_max_body_size 4m; # skin uploads
|
||||
|
||||
@@ -80,13 +91,13 @@ server {
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name pack.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/pack.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/pack.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
@@ -98,13 +109,13 @@ server {
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name distribution.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
@@ -114,3 +125,44 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name avatar.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/avatar.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/avatar.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name status.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/status.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/status.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Uptime Kuma uses websockets (socket.io) for live status updates.
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Ulicraft host nginx — TLS ingress with Let's Encrypt certs.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Template. Render with BOTH vars then install:
|
||||
#
|
||||
# APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 BASE_DOMAIN=ulicraft.net \
|
||||
# envsubst '$APP_DIR $BASE_DOMAIN' \
|
||||
# < nginx/ulicraft.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
|
||||
# sudo ln -sf /etc/nginx/sites-available/ulicraft.conf /etc/nginx/sites-enabled/
|
||||
# sudo nginx -t && sudo systemctl reload nginx
|
||||
#
|
||||
# Prereqs:
|
||||
# - certs issued: tooling/issue-letsencrypt.sh -> $APP_DIR/certs/<name>/{cert,key}.pem
|
||||
# - the docker stack up with the nginx override (publishes drasl on 127.0.0.1:25585):
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
# - nginx's user (www-data) can READ $APP_DIR/{www,pack,custom,certs}
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# HTTP -> HTTPS for every name.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${BASE_DOMAIN} auth.${BASE_DOMAIN} pack.${BASE_DOMAIN};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# Apex — static landing page (./www) + launcher downloads (./mirror/launcher).
|
||||
# /ca.crt from the caddy path is intentionally omitted: LE certs are publicly
|
||||
# trusted, so guests need no CA import.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name ${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/${BASE_DOMAIN}/key.pem;
|
||||
|
||||
# Launcher downloads (populated by tooling/fetch-launcher.sh -> ./mirror/launcher).
|
||||
location /launcher/ {
|
||||
alias ${APP_DIR}/mirror/launcher/;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location / {
|
||||
root ${APP_DIR}/www;
|
||||
index index.html;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
|
||||
# Pack — packwiz metadata (./pack) + custom jars (./custom at /custom/).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name pack.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/pack.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/pack.${BASE_DOMAIN}/key.pem;
|
||||
|
||||
# Locally-hosted custom mod jars live outside ./pack; expose them at /custom/.
|
||||
location /custom/ {
|
||||
alias ${APP_DIR}/custom/;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location / {
|
||||
root ${APP_DIR}/pack;
|
||||
autoindex on;
|
||||
}
|
||||
}
|
||||
|
||||
# Auth — reverse proxy to drasl (published on localhost by the nginx override).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name auth.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/auth.${BASE_DOMAIN}/key.pem;
|
||||
|
||||
client_max_body_size 4m; # skin uploads
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:25585;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
37
nmsr/config.toml.tmpl
Normal file
37
nmsr/config.toml.tmpl
Normal file
@@ -0,0 +1,37 @@
|
||||
# NMSR-aas config — renders to nmsr/config.toml (gitignored) via
|
||||
# tooling/render-config.sh. Only ${BASE_DOMAIN} is substituted.
|
||||
#
|
||||
# Skins come from Drasl, NOT Mojang. Drasl exposes Mojang-compatible routes at
|
||||
# the bare BaseURL:
|
||||
# session_server -> {url}/session/minecraft/profile/{uuid}
|
||||
# mojang_api_server -> {url}/users/profiles/minecraft/{name}
|
||||
# and embeds absolute skin URLs (http://auth.${BASE_DOMAIN}/web/texture/skin/…)
|
||||
# in the profile, which NMSR fetches directly (textures_server is only a
|
||||
# fallback). NMSR resolves auth.${BASE_DOMAIN} internally over the docker
|
||||
# network (caddy holds the mcnet alias → drasl), so no public round-trip.
|
||||
|
||||
[server]
|
||||
address = "0.0.0.0"
|
||||
port = 8080
|
||||
|
||||
[caching]
|
||||
cleanup_interval = "1h"
|
||||
resolve_cache_duration = "15m"
|
||||
texture_cache_duration = "48h"
|
||||
|
||||
[caching.cache_biases]
|
||||
|
||||
[mojank]
|
||||
# Point every Mojang endpoint at Drasl (internal, plain http via the caddy alias).
|
||||
session_server = "http://auth.${BASE_DOMAIN}"
|
||||
textures_server = "http://auth.${BASE_DOMAIN}"
|
||||
mojang_api_server = "http://auth.${BASE_DOMAIN}"
|
||||
session_server_rate_limit = 10
|
||||
# This is an offline-mode (authlib-injector) server — Drasl issues v3 offline
|
||||
# UUIDs, so they must be accepted.
|
||||
allow_offline_mode_uuids = true
|
||||
use_dashless_uuids = false
|
||||
|
||||
[rendering]
|
||||
sample_count = 1
|
||||
use_smaa = true
|
||||
@@ -1,3 +1,3 @@
|
||||
# Ulicraft modpack (packwiz)
|
||||
|
||||
This directory is the **single source of truth** for the modpack (MC 1.21.1, NeoForge 21.1.209). Curate mods from here with `packwiz modrinth add <slug>` (prefer Modrinth over CurseForge); each mod produces a `mods/<slug>.pw.toml` metadata file referencing the upstream CDN. **Client-only mods must be tagged `side = "client"`** in their `.pw.toml` so the itzg server does not pull and crash on them. After adding/removing mods run `packwiz refresh` to recompute `index.toml`; for offline LAN play run `tooling/mirror-mods.sh` to vendor the jars and rewrite the download URLs to the local Caddy mirror. Curation is an ongoing manual task — no mods are added yet.
|
||||
This directory is the **single source of truth** for the modpack (MC 1.21.1, NeoForge 21.1.209). Curate mods from here with `packwiz modrinth add <slug>` (prefer Modrinth over CurseForge); each mod produces a `mods/<slug>.pw.toml` metadata file referencing the upstream CDN. **Client-only mods must be tagged `side = "client"`** in their `.pw.toml` so the itzg server does not pull and crash on them. After adding/removing mods run `packwiz refresh` to recompute `index.toml`. Clients and the itzg server fetch jars from the upstream CDN referenced in each `.pw.toml`. Curation is an ongoing manual task — no mods are added yet.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user