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 |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: deploy
|
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`
|
# Deploy Ulicraft to `cochi`
|
||||||
@@ -13,8 +13,9 @@ Redeploy the running stack on the production host. Full reference:
|
|||||||
- Host: `cochi` (SSH alias)
|
- Host: `cochi` (SSH alias)
|
||||||
- Path: `/home/ubuntu/mc/ulicraft-server-v1`
|
- Path: `/home/ubuntu/mc/ulicraft-server-v1`
|
||||||
- Branch: `main`
|
- Branch: `main`
|
||||||
- Run mode: `online` → compose files `docker-compose.yml` + `docker-compose.static.yml`
|
- Stack: single unified `docker-compose.yml` (drasl, minecraft, mc-backup,
|
||||||
- Auth: Drasl (base stack)
|
caddy, nmsr, uptime-kuma)
|
||||||
|
- Auth: Drasl
|
||||||
- Restart: **hard** (down → up) — players are disconnected for a few minutes
|
- Restart: **hard** (down → up) — players are disconnected for a few minutes
|
||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
@@ -35,32 +36,36 @@ invoking this skill is the authorization to proceed, but:
|
|||||||
ssh cochi 'set -e
|
ssh cochi 'set -e
|
||||||
cd /home/ubuntu/mc/ulicraft-server-v1
|
cd /home/ubuntu/mc/ulicraft-server-v1
|
||||||
git pull --ff-only
|
git pull --ff-only
|
||||||
docker compose -f docker-compose.yml -f docker-compose.static.yml down --remove-orphans
|
tooling/render-config.sh
|
||||||
tooling/build-stack.sh --up online'
|
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
|
- `--ff-only` refuses to merge — if the pull is not a fast-forward, STOP and
|
||||||
report (local changes on the host need manual resolution).
|
report (local changes on the host need manual resolution).
|
||||||
- `build-stack.sh --up online` renders configs (needs `.env` complete:
|
- `render-config.sh` re-renders drasl/nmsr/packwiz configs from `.env`
|
||||||
`BASE_DOMAIN`, `HOST_LAN_IP`, `RCON_PASSWORD`) and runs `compose up -d`.
|
(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.**
|
3. **Verify.**
|
||||||
```bash
|
```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
|
Confirm `caddy`, `drasl`, `minecraft`, `mc-backup`, `nmsr`, `uptime-kuma` are
|
||||||
log briefly and confirm it reaches `Done` (server ready):
|
Up. Tail the minecraft log briefly and confirm it reaches `Done` (server
|
||||||
|
ready):
|
||||||
```bash
|
```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
|
## Guardrails
|
||||||
|
|
||||||
- Never `git reset --hard` or discard changes on `cochi` without telling the user
|
- Never `git reset --hard` or discard changes on `cochi` without telling the user
|
||||||
first — there may be host-local edits.
|
first — there may be host-local edits.
|
||||||
- Volumes (`mc_data`, `drasl_state`) persist across `down`; the world is safe. Do
|
- Volumes (`mc_data`, `drasl_state`, `kuma_data`) persist across `down`; the
|
||||||
NOT pass `-v`/`--volumes` to `down`.
|
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
|
- If a step fails, STOP and surface the exact error + the failing command. Do not
|
||||||
improvise recovery on production.
|
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.
|
|
||||||
+12
-35
@@ -1,35 +1,22 @@
|
|||||||
# Copy to .env and fill in real values
|
# Copy to .env and fill in real values
|
||||||
|
|
||||||
# Single base domain for the whole stack; every service is a subdomain of this.
|
# 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
|
# DNS is handled OUTSIDE this repo — point ${BASE_DOMAIN} and every subdomain
|
||||||
# won't resolve through a normal unicast DNS server.
|
# (auth. pack. avatar. status. distribution. www.) at the host running nginx.
|
||||||
BASE_DOMAIN=ulicraft.lan
|
BASE_DOMAIN=ulicraft.net
|
||||||
# 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
|
|
||||||
|
|
||||||
RCON_PASSWORD=change-me-to-something-random
|
RCON_PASSWORD=change-me-to-something-random
|
||||||
|
|
||||||
# Caddy host-published port (docker-compose.caddy.yml). Default 80 for the
|
# caddy host-published port. The host's own nginx terminates TLS and
|
||||||
# standalone LAN/air-gap path. When the machine's own nginx fronts caddy
|
# reverse-proxies to caddy by Host header (nginx/ulicraft-caddy.conf.tmpl), so
|
||||||
# (nginx terminates TLS → reverse-proxies to caddy), set a high port and bind
|
# bind caddy to a high localhost-only port — only nginx should reach it.
|
||||||
# it to localhost so only nginx reaches it:
|
CADDY_HTTP_PORT=8880
|
||||||
# CADDY_HTTP_PORT=8880
|
CADDY_HTTP_BIND=127.0.0.1
|
||||||
# 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
|
|
||||||
|
|
||||||
# distribution.${BASE_DOMAIN} static vhost (docker-compose.distribution.yml).
|
# distribution.${BASE_DOMAIN} static vhost. Absolute host path to the built
|
||||||
# Absolute host path to the built static site — it lives in ANOTHER repo.
|
# static site — it lives in ANOTHER repo. Bind-mounted read-only into caddy.
|
||||||
# Bind-mounted read-only into caddy and served at distribution.${BASE_DOMAIN}.
|
|
||||||
DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distribution/dist
|
DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distribution/dist
|
||||||
|
|
||||||
# Only needed if using CurseForge-exclusive mods:
|
# Only needed if using CurseForge-exclusive mods:
|
||||||
# CF_API_KEY=your-curseforge-api-key
|
# 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.
|
# Only needed to issue real TLS certs. DNS-01 needs no inbound ports.
|
||||||
LE_EMAIL=you@example.com
|
LE_EMAIL=you@example.com
|
||||||
# space-separated subdomains; apex ${BASE_DOMAIN} is always included
|
# 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 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_CK can be blank on first run — acme.sh prints an auth URL, then paste it.
|
||||||
OVH_END_POINT=ovh-eu
|
OVH_END_POINT=ovh-eu
|
||||||
@@ -45,13 +32,3 @@ OVH_AK=
|
|||||||
OVH_AS=
|
OVH_AS=
|
||||||
OVH_CK=
|
OVH_CK=
|
||||||
# LE_STAGING=1 # use the untrusted staging CA for dry runs
|
# 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
|
|
||||||
+3
-6
@@ -5,15 +5,15 @@ runtime/authlib-injector.jar
|
|||||||
|
|
||||||
# rendered configs (from tooling/render-config.sh)
|
# rendered configs (from tooling/render-config.sh)
|
||||||
drasl/config/config.toml
|
drasl/config/config.toml
|
||||||
dnsmasq/dnsmasq.conf
|
nmsr/config.toml
|
||||||
# packwiz pack files rendered from pack/**/*.tmpl by tooling/render-pack.sh
|
# packwiz pack files rendered from pack/**/*.tmpl by tooling/render-pack.sh
|
||||||
# (domain-dependent build output); source of truth is the .tmpl + custom/ jars.
|
# (domain-dependent build output); source of truth is the .tmpl + custom/ jars.
|
||||||
pack/mods/*.pw.toml
|
pack/mods/*.pw.toml
|
||||||
pack/index.toml
|
pack/index.toml
|
||||||
pack/CustomSkinLoader/CustomSkinLoader.json
|
pack/CustomSkinLoader/CustomSkinLoader.json
|
||||||
|
|
||||||
# vendored / mirrored binaries
|
# vendored binaries
|
||||||
mirror/
|
launcher/
|
||||||
pack/mods-files/
|
pack/mods-files/
|
||||||
|
|
||||||
# landing page: generated build output + deps
|
# landing page: generated build output + deps
|
||||||
@@ -22,8 +22,5 @@ landing/node_modules/
|
|||||||
landing/.astro/
|
landing/.astro/
|
||||||
landing/dist/
|
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)
|
# Let's Encrypt certs + private keys (issued by tooling/issue-letsencrypt.sh)
|
||||||
certs/
|
certs/
|
||||||
@@ -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
|
> Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
|
||||||
> persistent auth/skin infrastructure designed to outlive the LAN weekend
|
> (Drasl), a packwiz modpack, avatar rendering, a guest landing page, and uptime
|
||||||
> and integrate with the existing homelab.
|
> 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
|
## Goals & Constraints
|
||||||
|
|
||||||
- **Players**: 4–10 friends, LAN-based
|
- **Players**: 4–10 friends
|
||||||
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
|
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
|
||||||
- **Pack approach**: Manually curated (~50–100 mods), NOT a forked existing pack
|
- **Pack approach**: Manually curated (~50–100 mods), NOT a forked existing pack
|
||||||
- **Minecraft version**: 1.21.1
|
- **Minecraft version**: 1.21.1, **NeoForge** (not classic Forge)
|
||||||
- **Mod loader**: NeoForge (NOT classic Forge — see Decisions below)
|
- **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC
|
||||||
- **Network**: LAN party context, but stack is persistent (lives past the weekend)
|
- **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host
|
||||||
- **Auth**: Self-hosted Yggdrasil-compatible (Drasl) + Keycloak OIDC
|
- **Persistence horizon**: long-term service, not disposable
|
||||||
- **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`.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
One unified `docker-compose.yml`. The host's own nginx terminates TLS (Let's
|
||||||
LAN segment
|
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
|
||||||
├── AdGuard Home → resolves drasl.home.local, keycloak.home.local
|
via caddy's `mcnet` aliases (`auth.`, `pack.`).
|
||||||
│ 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
|
|
||||||
|
|
||||||
Clients (LAN party guests):
|
```
|
||||||
Prism Launcher → authlib-injector account pointing at Drasl
|
Internet ── host nginx (TLS, LE certs, HSTS) ──► caddy (127.0.0.1:8880)
|
||||||
→ joins minecraft:25565
|
│ 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.
|
## Key Decisions
|
||||||
2. Clicks "Register with Keycloak" → redirected to Keycloak login.
|
|
||||||
3. Logs in with homelab Keycloak credentials.
|
- **NeoForge over Forge** — the 1.21.x kitchen-sink ecosystem is overwhelmingly
|
||||||
4. Returned to Drasl, picks a player name, uploads a skin.
|
NeoForge; the Forge team migrated. `itzg/minecraft-server` via `TYPE=NEOFORGE`.
|
||||||
5. Drasl shows them a "Minecraft Token" on their profile page.
|
- **Drasl over Ely.by / vanilla offline** — self-hosted Go service, Yggdrasil
|
||||||
6. In Prism Launcher: Settings → Accounts → Add authlib-injector account
|
drop-in, skins + capes, actively maintained. Run with **password login**
|
||||||
with URL `http://drasl.home.local:25585/authlib-injector` and the
|
(no OIDC/Keycloak in this stack).
|
||||||
Minecraft Token as password.
|
- **Manual curation** (user choice, flagged expensive) — if it gets painful,
|
||||||
7. Joins Minecraft server. Server validates session against Drasl (via
|
fork-and-trim Leaking Kitchen Sink is still on the table (closest vibe match).
|
||||||
authlib-injector JVM agent), Drasl confirms, player enters with their skin.
|
- **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
|
## Critical Gotchas
|
||||||
|
|
||||||
### Minecraft 1.21+ secure profile incompatibility
|
### Minecraft 1.21+ secure profile
|
||||||
- Server: must set `enforce-secure-profile=false` (compose: `ENFORCE_SECURE_PROFILE=FALSE`)
|
- Server: `ENFORCE_SECURE_PROFILE=FALSE` (`enforce-secure-profile=false`)
|
||||||
- Drasl: must set `SignPublicKeys = false`
|
- Drasl: `SignPublicKeys = false`
|
||||||
- These are linked. The Drasl docs explicitly warn: *"Mixed authentication does
|
- Linked. Drasl docs: *"Mixed authentication does not work with
|
||||||
not work with `SignPublicKeys = true` on Minecraft 1.21+."*
|
`SignPublicKeys = true` on Minecraft 1.21+."*
|
||||||
|
|
||||||
### authlib-injector JVM agent
|
### authlib-injector JVM agent
|
||||||
- Syntax: `-javaagent:/path/to/authlib-injector.jar=<yggdrasil-api-url>`
|
- Syntax: `-javaagent:/extras/authlib-injector.jar=<yggdrasil-api-url>`
|
||||||
- The URL after `=` is the **API root**, which for Drasl is
|
- URL after `=` is the **API root** = `<BaseURL>/authlib-injector`. For this
|
||||||
`<BaseURL>/authlib-injector`.
|
stack: `http://auth.${BASE_DOMAIN}/authlib-injector` (internal, over mcnet).
|
||||||
- **Must match between server and client.** If server uses
|
- **Must match between server and client** (clients use the public
|
||||||
`http://drasl.home.local:25585/authlib-injector` and client uses anything
|
`https://auth.${BASE_DOMAIN}/authlib-injector`), else session validation fails
|
||||||
else (e.g. an IP), session validation fails silently with "Invalid session".
|
silently with "Invalid session".
|
||||||
- Download from: https://github.com/yushijinhun/authlib-injector/releases
|
- Releases: https://github.com/yushijinhun/authlib-injector — jar lives in
|
||||||
- Mount into the itzg container at `/extras/authlib-injector.jar`.
|
`runtime/` (mounted 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.
|
|
||||||
|
|
||||||
### NeoForge version pinning
|
### NeoForge version pinning
|
||||||
Mods are picky about exact NeoForge minor versions. The compose currently
|
Mods are picky about exact minor versions. Compose pins `NEOFORGE_VERSION:
|
||||||
pins `NEOFORGE_VERSION: "21.1.209"` as a placeholder. **Verify against
|
"21.1.209"`. **Verify against https://projects.neoforged.net/neoforged/neoforge
|
||||||
https://projects.neoforged.net/neoforged/neoforge before deploying.** Don't
|
before deploying.** Never `"latest"` for a stable server.
|
||||||
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.
|
|
||||||
|
|
||||||
### Mod source preference
|
### Mod source preference
|
||||||
Prefer **Modrinth over CurseForge** for itzg auto-download. CurseForge
|
Prefer **Modrinth over CurseForge** (`packwiz modrinth add <slug>`). CF needs
|
||||||
requires an API key (`CF_API_KEY`) and has been historically flakier with
|
`CF_API_KEY` and is flakier. Tag client-only mods `side = "client"` so the server
|
||||||
the itzg image. Use CF only for mods that are exclusive to it.
|
doesn't pull and crash on them.
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Memory sizing
|
### Memory sizing
|
||||||
With ~50–100 mods and 8 concurrent players on 1.21.1:
|
~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
|
||||||
- `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G` is a safe starting point
|
`USE_AIKAR_FLAGS: TRUE`.
|
||||||
- Use `USE_AIKAR_FLAGS: TRUE` for the well-known GC tuning
|
|
||||||
- Monitor with `mc-monitor` (itzg makes one) if you want metrics
|
|
||||||
|
|
||||||
### Backups
|
## Config (.env)
|
||||||
`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.
|
|
||||||
|
|
||||||
## 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**
|
Launcher: **FjordLauncherUnlocked** (Prism fork, first-class authlib-injector),
|
||||||
- Client type: `OpenID Connect`
|
mirrored at apex `/launcher/`. Per guest:
|
||||||
- Client ID: `drasl`
|
1. Download from `https://${BASE_DOMAIN}` → `/launcher/`.
|
||||||
- Name: `Drasl Minecraft Auth`
|
2. Add an authlib-injector account, URL
|
||||||
|
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
|
||||||
2. **Capability config**
|
`https://auth.${BASE_DOMAIN}`).
|
||||||
- Client authentication: **ON** (confidential client)
|
3. Import the pack `https://pack.${BASE_DOMAIN}/pack.toml`.
|
||||||
- Authentication flow: **Standard flow** only
|
4. Connect to `${BASE_DOMAIN}` (`:25565`).
|
||||||
- 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
|
|
||||||
|
|
||||||
## Open / Pending Work
|
## Open / Pending Work
|
||||||
|
|
||||||
These were on the roadmap when we ended the brainstorm:
|
- [ ] **Mod shortlist** for the kitchen-sink pack: Performance (Embeddium,
|
||||||
|
FerriteCore, ModernFix), Tech (Mekanism, Create, IE, AE2), Magic (Ars Nouveau,
|
||||||
- [ ] **Mod shortlist for the kitchen-sink pack.** Categories to fill:
|
Botania, Iron's Spells), Storage (Sophisticated, Functional), Exploration
|
||||||
- Performance (Embeddium/Sodium-equivalent for NeoForge, FerriteCore, ModernFix)
|
(YUNG's, Repurposed Structures), QoL (JEI, Jade, IPN, AppleSkin), World gen
|
||||||
- Tech/automation (Mekanism, Create, Immersive Engineering, AE2)
|
(Tectonic, Terralith — verify NeoForge 1.21.1), Compat (Polymorph), Map (Xaero),
|
||||||
- Magic (Ars Nouveau, Botania, Iron's Spells 'n Spellbooks)
|
Voice (Simple Voice Chat — 24454/udp already in compose).
|
||||||
- Storage (Sophisticated Storage/Backpacks, Functional Storage)
|
- [ ] Server-side perf mods (C2ME, etc.).
|
||||||
- Exploration (YUNG's structures, Repurposed Structures, biome packs)
|
- [ ] **Verify NeoForge version** against current stable before first deploy.
|
||||||
- QoL (JEI, Jade, JEI Resources, Inventory Profiles Next, AppleSkin)
|
- [ ] **Bootstrap Drasl admin** account.
|
||||||
- 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
|
|
||||||
|
|
||||||
## Reference URLs
|
## Reference URLs
|
||||||
|
|
||||||
- Drasl repo: https://github.com/unmojang/drasl
|
- Drasl: https://github.com/unmojang/drasl — config docs:
|
||||||
- Drasl configuration docs: https://github.com/unmojang/drasl/blob/master/doc/configuration.md
|
https://github.com/unmojang/drasl/blob/master/doc/configuration.md
|
||||||
- Drasl recipes (example configs): https://github.com/unmojang/drasl/blob/master/doc/recipes.md
|
|
||||||
- authlib-injector: https://github.com/yushijinhun/authlib-injector
|
- authlib-injector: https://github.com/yushijinhun/authlib-injector
|
||||||
- itzg/minecraft-server: https://github.com/itzg/docker-minecraft-server
|
- 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
|
- itzg/mc-backup: https://github.com/itzg/docker-mc-backup
|
||||||
- NeoForge versions: https://projects.neoforged.net/neoforged/neoforge
|
- NeoForge versions: https://projects.neoforged.net/neoforged/neoforge
|
||||||
- Prism Launcher: https://prismlauncher.org/
|
- NMSR: https://github.com/NickAcPT/nmsr-rs
|
||||||
- Reference modpacks for inspiration (NOT forking):
|
- Uptime Kuma: https://github.com/louislam/uptime-kuma
|
||||||
- ATM10 (Modrinth/CurseForge — ~500 mods, NeoForge 1.21.1)
|
- FjordLauncherUnlocked: https://github.com/hero-persson/FjordLauncherUnlocked
|
||||||
- Leaking Kitchen Sink (CurseForge — trimmed ~150 mods, NeoForge 1.21.1)
|
- Reference packs (inspiration, NOT forking): ATM10, Leaking Kitchen Sink.
|
||||||
|
|
||||||
## Communication Preferences (carry-over from past sessions)
|
## Communication Preferences (carry-over)
|
||||||
|
|
||||||
- Concise and direct
|
- Concise and direct; diagnose + resolve without demanding repro steps.
|
||||||
- Diagnose and resolve without demanding detailed reproduction steps
|
- Prefer iterative single-change updates over large rewrites.
|
||||||
- Prefer iterative single-change updates over large rewrites
|
- Cautious scripts that halt on ambiguity rather than proceeding silently.
|
||||||
- Cautious scripts that halt on ambiguity rather than proceeding silently
|
- Avoid over-engineering when a simple answer suffices.
|
||||||
- Avoid over-engineered responses when a simple answer suffices
|
- Spanish or English both fine.
|
||||||
- Spanish or English fine; user is comfortable in both
|
|
||||||
@@ -1,111 +1,159 @@
|
|||||||
# Ulicraft Server
|
# Ulicraft Server
|
||||||
|
|
||||||
Self-hosted modded Minecraft (NeoForge 1.21.1) for a LAN party. Runs **online**
|
Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
|
||||||
(internet present) or **fully air-gapped** (offline). Self-hosted auth/skins
|
(Drasl), a packwiz modpack, avatar rendering, a guest landing page, and uptime
|
||||||
(Drasl), a packwiz modpack, a guest landing page, and a transparent mirror of
|
monitoring. One unified `docker-compose.yml` runs the whole stack behind the
|
||||||
Mojang/launcher/NeoForge assets so guest laptops need no internet.
|
host's nginx + Let's Encrypt.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
|
One compose file, one `docker compose up -d`:
|
||||||
|
|
||||||
| Service | Image | Role |
|
| Service | Image | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `minecraft` | itzg/minecraft-server | NeoForge 1.21.1 server, `:25565` |
|
| `minecraft` | itzg/minecraft-server | NeoForge 1.21.1 server, `:25565` |
|
||||||
| `drasl` | unmojang/drasl | Yggdrasil auth + skins (password login) |
|
| `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 |
|
| `mc-backup` | itzg/mc-backup | world backups every 6h via RCON |
|
||||||
|
|
||||||
Optional layers: `avahi` (mDNS `.local`), `dnsmasq` (turnkey DNS) — only if you
|
Vhosts (all proxied through the host nginx → caddy):
|
||||||
don't run your own DNS.
|
|
||||||
|
|
||||||
Config lives in `.env`: `BASE_DOMAIN` (default `ulicraft.lan`), `HOST_LAN_IP`,
|
- apex `${BASE_DOMAIN}` — landing page + `/launcher/` downloads
|
||||||
`RCON_PASSWORD`, `ENABLE_MDNS`. Services are subdomains: `auth.`, `packwiz.`,
|
- `auth.` — Drasl
|
||||||
`mc.`, plus the apex landing page.
|
- `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
|
Handled **outside this repo**. Point `${BASE_DOMAIN}` and every subdomain
|
||||||
tooling/dns-records.sh online # service names only (internet present)
|
(`auth. pack. avatar. status. distribution. www.`) at the host running nginx.
|
||||||
tooling/dns-records.sh airgap # + Mojang/launcher/NeoForge spoofs (offline)
|
|
||||||
```
|
|
||||||
Paste the printed records into your DNS. **Use `.lan`, not `.local`** — `.local`
|
|
||||||
is intercepted by mDNS and won't resolve via unicast DNS.
|
|
||||||
|
|
||||||
Alternatives: `ENABLE_MDNS=true` + `BASE_DOMAIN=*.local` (zero-config mDNS,
|
|
||||||
macOS/Linux native, Windows needs Bonjour) — but mDNS can't serve the air-gap
|
|
||||||
upstream spoofs. Or layer in `docker-compose.dnsmasq.yml` for a turnkey DNS.
|
|
||||||
|
|
||||||
### Simulate the DNS via /etc/hosts (single machine, testing)
|
|
||||||
|
|
||||||
No DNS server? Append the records straight to `/etc/hosts` (marker-wrapped):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tooling/dns-records.sh online hosts | sudo tee -a /etc/hosts # services only
|
|
||||||
tooling/dns-records.sh airgap hosts | sudo tee -a /etc/hosts # + Mojang spoofs
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove them later:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo sed -i '/# >>> ulicraft/,/# <<< ulicraft/d' /etc/hosts
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- `/etc/hosts` has **no wildcard** — only the listed names resolve.
|
|
||||||
- `airgap` adds the Mojang/launcher/NeoForge hostnames pointing at the LAN; this
|
|
||||||
**breaks normal internet access to those domains** while present. Use it only
|
|
||||||
while offline; for an online box use `online`.
|
|
||||||
- `mc.<domain>` works (client defaults to :25565); SRV isn't used via `/etc/hosts`.
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Docker + Docker Compose; for prep also `pnpm`, `packwiz`, `jq`, `curl`,
|
Docker + Docker Compose; for prep also `pnpm`, `packwiz`, `jq`, `curl`,
|
||||||
`envsubst`, `sha1sum`.
|
`envsubst`.
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
```sh
|
```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,
|
# One-time / on change — render configs + build content:
|
||||||
# mirror mods+assets, fetch launcher +
|
tooling/render-config.sh # drasl + nmsr + packwiz configs from *.tmpl
|
||||||
# authlib, pre-bake the server volume
|
( 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
|
docker compose up -d --build # first run installs NeoForge + mods, builds nmsr
|
||||||
tooling/build-stack.sh --up airgap # offline; full asset mirror on :443 (default)
|
docker compose down # stop
|
||||||
tooling/build-stack.sh --up core # base only (debugging)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`--up` prints the DNS records to add for that mode. Down:
|
First boot: itzg installs NeoForge + the packwiz mods into the `mc_data` volume;
|
||||||
`docker compose -f docker-compose.yml -f docker-compose.static.yml -f docker-compose.mirror.yml down`
|
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)
|
## Join (guests)
|
||||||
|
|
||||||
1. Open `http://ulicraft.lan`, download FjordLauncherUnlocked for your OS.
|
1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS.
|
||||||
2. Add an **authlib-injector** account, URL `http://auth.ulicraft.lan/authlib-injector`
|
2. Add an **authlib-injector** account, URL
|
||||||
(register/login at `http://auth.ulicraft.lan`).
|
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
|
||||||
3. **Air-gap only:** trust the local CA — download `http://ulicraft.lan/ca.crt`
|
`https://auth.${BASE_DOMAIN}`).
|
||||||
and add it to your OS trust store (so the HTTPS asset mirror is trusted).
|
3. Import the pack: `https://pack.${BASE_DOMAIN}/pack.toml`.
|
||||||
4. Import the pack: `http://packwiz.ulicraft.lan/pack.toml`.
|
4. Connect to **`${BASE_DOMAIN}`** (Minecraft, `:25565`).
|
||||||
5. Connect to **`mc.ulicraft.lan`** (no port needed).
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose.yml # core: drasl, minecraft, mc-backup, caddy
|
docker-compose.yml # the whole stack (one file)
|
||||||
.static.yml .mirror.yml # online/airgap layers
|
caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets
|
||||||
.avahi.yml .dnsmasq.yml # optional DNS layers
|
# (core/static/distribution/avatar/status)
|
||||||
caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets (core/static/mirror)
|
|
||||||
drasl/config/config.toml.tmpl # auth config (rendered)
|
drasl/config/config.toml.tmpl # auth config (rendered)
|
||||||
dnsmasq/dnsmasq.conf.tmpl # optional DNS config (rendered)
|
nmsr/config.toml.tmpl # avatar renderer config, Drasl-backed (rendered)
|
||||||
docker/avahi/ # mDNS responder image
|
docker/nmsr/ # built-from-source NMSR image
|
||||||
pack/ # packwiz modpack source
|
pack/ # packwiz modpack source
|
||||||
landing/ # Astro site → www/
|
landing/ # Astro site → www/
|
||||||
tooling/ # build-stack, render-config, dns-records,
|
launcher/ # FjordLauncher assets (gitignored)
|
||||||
# mirror-mods, mirror-airgap, fetch-launcher
|
tooling/ # render-config, render-pack, render-nginx,
|
||||||
mirror/ # vendored jars + launcher + asset mirror (gitignored)
|
# issue-letsencrypt, fetch-launcher, add-custom-mod
|
||||||
plan/ # full design docs (00–12)
|
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.
|
See `plan/00-overview.md` for the full design.
|
||||||
+9
-7
@@ -1,10 +1,12 @@
|
|||||||
# Base Caddy config. Vhosts live in conf.d/*.caddy snippets that are mounted
|
# Base Caddy config. Vhosts live in conf.d/*.caddy snippets:
|
||||||
# in selectively so the stack can run with or without the static site / mirror:
|
# conf.d/00-core.caddy auth + packwiz
|
||||||
# conf.d/00-core.caddy auth + packwiz (always — core ingress)
|
# conf.d/10-static.caddy apex landing + launcher downloads
|
||||||
# conf.d/10-static.caddy apex landing + launcher downloads (static toggle)
|
# conf.d/30-distribution.caddy static site from another repo
|
||||||
# conf.d/20-mirror.caddy air-gap upstream mirror (tls internal) (mirror toggle)
|
# conf.d/40-avatar.caddy nmsr skin/avatar renderer
|
||||||
# auto_https disable_redirects: http:// sites stay plaintext on :80; the mirror
|
# conf.d/50-status.caddy uptime-kuma status page
|
||||||
# snippet's hosts still get internal TLS on :443 when that snippet is present.
|
# 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
|
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.
|
# Apex — landing page (/srv/www) + launcher downloads (/srv/launcher).
|
||||||
# Mounted only when the stack runs with static files (build-stack.sh up static|full).
|
|
||||||
http://{$BASE_DOMAIN} {
|
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
|
# Launcher downloads are a sibling mount (/srv/launcher), not nested under
|
||||||
# the read-only /srv/www. handle_path strips the /launcher prefix.
|
# the read-only /srv/www. handle_path strips the /launcher prefix.
|
||||||
handle_path /launcher/* {
|
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.
|
# Distribution — static site served from a web root in ANOTHER repo. The host
|
||||||
# The host path is set by DISTRIBUTION_WEB_ROOT in .env and bind-mounted to
|
# 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
|
# /srv/distribution by the caddy service in docker-compose.yml.
|
||||||
# set (see the distribution override).
|
|
||||||
http://distribution.{$BASE_DOMAIN} {
|
http://distribution.{$BASE_DOMAIN} {
|
||||||
root * /srv/distribution
|
root * /srv/distribution
|
||||||
file_server browse
|
file_server browse
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Avatar — NMSR skin/avatar renderer (nmsr service), Drasl-backed.
|
||||||
|
http://avatar.{$BASE_DOMAIN} {
|
||||||
|
reverse_proxy nmsr:8080
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Status — Uptime Kuma monitoring + public status page (uptime-kuma service).
|
||||||
|
http://status.{$BASE_DOMAIN} {
|
||||||
|
reverse_proxy uptime-kuma:3001
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
|
|
||||||
+91
-24
@@ -1,31 +1,31 @@
|
|||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# Ulicraft LAN-party stack — Minecraft 1.21.1 NeoForge + Drasl auth
|
# Ulicraft LAN-party stack — Minecraft 1.21.1 NeoForge + Drasl auth
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# Single source of config: BASE_DOMAIN + HOST_LAN_IP in .env.
|
# Single, unified compose. One file holds the whole stack:
|
||||||
# DNS is provided by YOUR party DNS server — see tooling/dns-records.sh for the
|
# drasl auth + skins (Yggdrasil), internal only
|
||||||
# records to add (online vs airgap). A turnkey dnsmasq is available as an
|
# minecraft the server (itzg/NEOFORGE), installs mods via packwiz
|
||||||
# optional layer: docker-compose.dnsmasq.yml.
|
# mc-backup world backups every 6h via RCON
|
||||||
# - ingress is layered on, NOT in this base file: caddy (docker-compose.caddy.yml,
|
# caddy internal ingress — routes auth./pack./apex/launcher/avatar.
|
||||||
# LAN/air-gap, holds mcnet aliases) OR the host's nginx (docker-compose.nginx.yml
|
# /distribution. by Host header (conf.d/*.caddy)
|
||||||
# + Let's Encrypt, production)
|
# nmsr skin/avatar renderer at avatar.${BASE_DOMAIN}
|
||||||
# - drasl handles auth (password login; NO Keycloak/OIDC in this stack)
|
# uptime-kuma status monitoring at status.${BASE_DOMAIN}
|
||||||
# - packwiz pack is served by the ingress at pack.${BASE_DOMAIN}
|
#
|
||||||
# - minecraft installs mods via PACKWIZ_URL, authlib-injector -> auth.
|
# 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:
|
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:
|
drasl:
|
||||||
image: unmojang/drasl:latest
|
image: unmojang/drasl:latest
|
||||||
container_name: drasl
|
container_name: drasl
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# Internal-only: no published host port. Reached via Caddy at
|
# Internal-only: no published host port. Reached via caddy at
|
||||||
# auth.${BASE_DOMAIN} (Caddy holds the network alias on mcnet).
|
# auth.${BASE_DOMAIN} (caddy holds the network alias on mcnet).
|
||||||
volumes:
|
volumes:
|
||||||
- ./drasl/config:/etc/drasl:ro
|
- ./drasl/config:/etc/drasl:ro
|
||||||
- drasl_state:/var/lib/drasl
|
- drasl_state:/var/lib/drasl
|
||||||
@@ -58,7 +58,7 @@ services:
|
|||||||
|
|
||||||
# ── Auth: offline mode + authlib-injector pointing at Drasl ─
|
# ── Auth: offline mode + authlib-injector pointing at Drasl ─
|
||||||
ONLINE_MODE: "FALSE"
|
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"
|
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=http://auth.${BASE_DOMAIN}/authlib-injector"
|
||||||
|
|
||||||
# ── Server config ───────────────────────────────────────────
|
# ── Server config ───────────────────────────────────────────
|
||||||
@@ -71,9 +71,7 @@ services:
|
|||||||
ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+
|
ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+
|
||||||
ALLOW_FLIGHT: "TRUE" # many tech mods need this
|
ALLOW_FLIGHT: "TRUE" # many tech mods need this
|
||||||
|
|
||||||
# ── Mod source: packwiz pack served by the ingress ──────────
|
# ── Mod source: packwiz pack served by caddy (alias pack.) ──
|
||||||
# http + caddy alias by default; the nginx override switches this to
|
|
||||||
# https + host-gateway (extra_hosts) for the production path.
|
|
||||||
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
|
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
|
||||||
|
|
||||||
# ── RCON for remote admin ───────────────────────────────────
|
# ── RCON for remote admin ───────────────────────────────────
|
||||||
@@ -86,11 +84,17 @@ services:
|
|||||||
- mc_data:/data
|
- mc_data:/data
|
||||||
- ./runtime:/extras:ro # authlib-injector.jar lives here
|
- ./runtime:/extras:ro # authlib-injector.jar lives here
|
||||||
depends_on:
|
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:
|
networks:
|
||||||
- mcnet
|
- mcnet
|
||||||
|
|
||||||
# Optional: backups
|
|
||||||
mc-backup:
|
mc-backup:
|
||||||
image: itzg/mc-backup
|
image: itzg/mc-backup
|
||||||
container_name: mc-backup
|
container_name: mc-backup
|
||||||
@@ -109,9 +113,72 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- mcnet
|
- 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:
|
volumes:
|
||||||
drasl_state:
|
drasl_state:
|
||||||
mc_data:
|
mc_data:
|
||||||
|
kuma_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
mcnet:
|
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
|
|
||||||
@@ -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.
|
# -> drasl/config/config.toml (gitignored). Only ${BASE_DOMAIN} is substituted.
|
||||||
# Reached only via Caddy at auth.${BASE_DOMAIN}; drasl has no published host port.
|
# 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}"
|
Domain = "auth.${BASE_DOMAIN}"
|
||||||
ListenAddress = "0.0.0.0:25585" # internal; Caddy reverse-proxies to it
|
ListenAddress = "0.0.0.0:25585" # internal; Caddy reverse-proxies to it
|
||||||
DefaultAdminUsernames = ["admin"]
|
DefaultAdminUsernames = ["admin"]
|
||||||
|
|||||||
@@ -24,19 +24,15 @@ export const site = {
|
|||||||
dust: true,
|
dust: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Connect target shown to guests. No port needed — a dnsmasq SRV record
|
// Connect target shown to guests. No port needed (defaults to 25565).
|
||||||
// (_minecraft._tcp.mc.<domain>) points clients at 25565.
|
serverAddress: `${BASE_DOMAIN}`,
|
||||||
serverAddress: `mc.${BASE_DOMAIN}`,
|
|
||||||
|
|
||||||
// Drasl auth web UI + authlib-injector API root.
|
// Drasl auth web UI + authlib-injector API root (public HTTPS).
|
||||||
authUrl: `http://auth.${BASE_DOMAIN}`,
|
authUrl: `https://auth.${BASE_DOMAIN}`,
|
||||||
authlibUrl: `http://auth.${BASE_DOMAIN}/authlib-injector`,
|
authlibUrl: `https://auth.${BASE_DOMAIN}/authlib-injector`,
|
||||||
|
|
||||||
// packwiz modpack entrypoint.
|
// packwiz modpack entrypoint (public HTTPS, pack. subdomain).
|
||||||
packwizUrl: `http://packwiz.${BASE_DOMAIN}/pack.toml`,
|
packwizUrl: `https://pack.${BASE_DOMAIN}/pack.toml`,
|
||||||
|
|
||||||
// Caddy local CA root — guests import this to trust the offline asset mirror.
|
|
||||||
caCertUrl: `http://${BASE_DOMAIN}/ca.crt`,
|
|
||||||
|
|
||||||
// Static server-list panel (Status section). No fake live count — see plan
|
// Static server-list panel (Status section). No fake live count — see plan
|
||||||
// 09-landing.md option B.
|
// 09-landing.md option B.
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export interface UI {
|
|||||||
pb: string; // after it
|
pb: string; // after it
|
||||||
copy: string;
|
copy: string;
|
||||||
registerPre: string;
|
registerPre: string;
|
||||||
caPre: string;
|
|
||||||
caLink: string;
|
|
||||||
caPost: string;
|
|
||||||
};
|
};
|
||||||
s3: { kicker: string; h: string; p: string };
|
s3: { kicker: string; h: string; p: string };
|
||||||
s4: {
|
s4: {
|
||||||
@@ -137,9 +134,6 @@ export const ui: Record<Lang, UI> = {
|
|||||||
pb: "account using this URL, then log in with your Ulicraft credentials.",
|
pb: "account using this URL, then log in with your Ulicraft credentials.",
|
||||||
copy: "Copy",
|
copy: "Copy",
|
||||||
registerPre: "Need credentials? Register at",
|
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: {
|
s3: {
|
||||||
kicker: "Modpack",
|
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.",
|
pb: "con esta URL y luego inicia sesión con tus credenciales de Ulicraft.",
|
||||||
copy: "Copiar",
|
copy: "Copiar",
|
||||||
registerPre: "¿Sin credenciales? Regístrate en",
|
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: {
|
s3: {
|
||||||
kicker: "Modpack",
|
kicker: "Modpack",
|
||||||
@@ -315,9 +306,6 @@ export const ui: Record<Lang, UI> = {
|
|||||||
pb: "kontu bat URL honekin, eta gero hasi saioa zure Ulicraft kredentzialekin.",
|
pb: "kontu bat URL honekin, eta gero hasi saioa zure Ulicraft kredentzialekin.",
|
||||||
copy: "Kopiatu",
|
copy: "Kopiatu",
|
||||||
registerPre: "Kredentzialik ez? Erregistratu hemen:",
|
registerPre: "Kredentzialik ez? Erregistratu hemen:",
|
||||||
caPre: "Internetik gabeko festa? Deskargatu eta fidatu",
|
|
||||||
caLink: "tokiko CA ziurtagiriaz",
|
|
||||||
caPost: "fitxategi-mirrorrak HTTPS bidez ibil dadin.",
|
|
||||||
},
|
},
|
||||||
s3: {
|
s3: {
|
||||||
kicker: "Modpacka",
|
kicker: "Modpacka",
|
||||||
|
|||||||
@@ -207,9 +207,6 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
<p>
|
<p>
|
||||||
{t.join.s2.registerPre} <a href={site.authUrl}>{site.authUrl}</a>.
|
{t.join.s2.registerPre} <a href={site.authUrl}>{site.authUrl}</a>.
|
||||||
</p>
|
</p>
|
||||||
<span class="hint">
|
|
||||||
{t.join.s2.caPre} <a href={site.caCertUrl}>{t.join.s2.caLink}</a> {t.join.s2.caPost}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# Ulicraft host nginx — TLS terminator in FRONT of caddy.
|
# Ulicraft host nginx — TLS terminator in FRONT of caddy.
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# Alternative to ulicraft.conf.tmpl. Instead of nginx serving the static
|
# caddy (in docker-compose.yml) is the single internal ingress for ALL vhosts;
|
||||||
# files + proxying only drasl, here caddy (the docker container) is the single
|
# this host nginx terminates TLS and reverse-proxies everything to caddy by Host
|
||||||
# ingress for ALL vhosts and nginx just terminates TLS and reverse-proxies
|
# header. Prefer tooling/render-nginx.sh over hand-rendering.
|
||||||
# everything to caddy by Host header.
|
|
||||||
#
|
#
|
||||||
# Caddy must be published on a localhost port — set in .env:
|
# Caddy must be published on a localhost port — set in .env:
|
||||||
# CADDY_HTTP_PORT=8880
|
# CADDY_HTTP_PORT=8880
|
||||||
# CADDY_HTTP_BIND=127.0.0.1
|
# CADDY_HTTP_BIND=127.0.0.1
|
||||||
# and the stack brought up with the caddy override (NOT the nginx override):
|
# and the stack brought up:
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
# docker compose up -d
|
||||||
# -f docker-compose.static.yml 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 \
|
# CADDY_HTTP_PORT=8880 BASE_DOMAIN=ulicraft.net APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 \
|
||||||
# envsubst '$CADDY_HTTP_PORT $BASE_DOMAIN $APP_DIR' \
|
# envsubst '$CADDY_HTTP_PORT $BASE_DOMAIN $APP_DIR' \
|
||||||
# < nginx/ulicraft-caddy.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
|
# < nginx/ulicraft-caddy.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
|
||||||
@@ -32,7 +30,7 @@ upstream ulicraft_caddy {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
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;
|
return 301 https://$host$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,13 +38,13 @@ server {
|
|||||||
# caddy routes by the forwarded Host header (apex -> www, auth -> drasl,
|
# caddy routes by the forwarded Host header (apex -> www, auth -> drasl,
|
||||||
# pack -> packwiz files).
|
# pack -> packwiz files).
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl http2;
|
||||||
http2 on;
|
|
||||||
server_name ${BASE_DOMAIN};
|
server_name ${BASE_DOMAIN};
|
||||||
|
|
||||||
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
|
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
|
||||||
ssl_certificate_key ${APP_DIR}/certs/${BASE_DOMAIN}/key.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
|
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 {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl http2;
|
||||||
http2 on;
|
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};
|
server_name auth.${BASE_DOMAIN};
|
||||||
|
|
||||||
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
|
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
|
||||||
ssl_certificate_key ${APP_DIR}/certs/auth.${BASE_DOMAIN}/key.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
|
client_max_body_size 4m; # skin uploads
|
||||||
|
|
||||||
@@ -80,13 +91,13 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl http2;
|
||||||
http2 on;
|
|
||||||
server_name pack.${BASE_DOMAIN};
|
server_name pack.${BASE_DOMAIN};
|
||||||
|
|
||||||
ssl_certificate ${APP_DIR}/certs/pack.${BASE_DOMAIN}/cert.pem;
|
ssl_certificate ${APP_DIR}/certs/pack.${BASE_DOMAIN}/cert.pem;
|
||||||
ssl_certificate_key ${APP_DIR}/certs/pack.${BASE_DOMAIN}/key.pem;
|
ssl_certificate_key ${APP_DIR}/certs/pack.${BASE_DOMAIN}/key.pem;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://ulicraft_caddy;
|
proxy_pass http://ulicraft_caddy;
|
||||||
@@ -98,13 +109,13 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl http2;
|
||||||
http2 on;
|
|
||||||
server_name distribution.${BASE_DOMAIN};
|
server_name distribution.${BASE_DOMAIN};
|
||||||
|
|
||||||
ssl_certificate ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/cert.pem;
|
ssl_certificate ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/cert.pem;
|
||||||
ssl_certificate_key ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/key.pem;
|
ssl_certificate_key ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/key.pem;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://ulicraft_caddy;
|
proxy_pass http://ulicraft_caddy;
|
||||||
@@ -114,3 +125,44 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
-1
@@ -1,3 +1,3 @@
|
|||||||
# Ulicraft modpack (packwiz)
|
# 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.
|
||||||
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user