Compare commits
17
Commits
| 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
|
||||
description: Redeploy the Ulicraft Minecraft stack to the production host `cochi` — git pull on main then a hard restart (compose down + build-stack.sh --up online). Use when the user says "deploy", "redeploy", "deploy to cochi", "push to prod", "ship it", or "restart the server" for this project. Causes brief Minecraft downtime.
|
||||
description: Redeploy the Ulicraft Minecraft stack to the production host `cochi` — git pull on main then a hard restart (compose down + up). Use when the user says "deploy", "redeploy", "deploy to cochi", "push to prod", "ship it", or "restart the server" for this project. Causes brief Minecraft downtime.
|
||||
---
|
||||
|
||||
# Deploy Ulicraft to `cochi`
|
||||
@@ -13,8 +13,9 @@ Redeploy the running stack on the production host. Full reference:
|
||||
- Host: `cochi` (SSH alias)
|
||||
- Path: `/home/ubuntu/mc/ulicraft-server-v1`
|
||||
- Branch: `main`
|
||||
- Run mode: `online` → compose files `docker-compose.yml` + `docker-compose.static.yml`
|
||||
- Auth: Drasl (base stack)
|
||||
- Stack: single unified `docker-compose.yml` (drasl, minecraft, mc-backup,
|
||||
caddy, nmsr, uptime-kuma)
|
||||
- Auth: Drasl
|
||||
- Restart: **hard** (down → up) — players are disconnected for a few minutes
|
||||
|
||||
## Procedure
|
||||
@@ -35,32 +36,36 @@ invoking this skill is the authorization to proceed, but:
|
||||
ssh cochi 'set -e
|
||||
cd /home/ubuntu/mc/ulicraft-server-v1
|
||||
git pull --ff-only
|
||||
docker compose -f docker-compose.yml -f docker-compose.static.yml down --remove-orphans
|
||||
tooling/build-stack.sh --up online'
|
||||
tooling/render-config.sh
|
||||
tooling/fetch-authlib.sh
|
||||
docker compose down --remove-orphans
|
||||
docker compose up -d --build'
|
||||
```
|
||||
- `--ff-only` refuses to merge — if the pull is not a fast-forward, STOP and
|
||||
report (local changes on the host need manual resolution).
|
||||
- `build-stack.sh --up online` renders configs (needs `.env` complete:
|
||||
`BASE_DOMAIN`, `HOST_LAN_IP`, `RCON_PASSWORD`) and runs `compose up -d`.
|
||||
- `render-config.sh` re-renders drasl/nmsr/packwiz configs from `.env`
|
||||
(needs `.env` complete: `BASE_DOMAIN`, `RCON_PASSWORD`,
|
||||
`DISTRIBUTION_WEB_ROOT`, `CADDY_HTTP_*`).
|
||||
- `fetch-authlib.sh` ensures `runtime/authlib-injector.jar` exists (gitignored;
|
||||
skips if already valid). Missing jar = minecraft crashloops with "Error
|
||||
opening zip file or JAR manifest missing".
|
||||
|
||||
3. **Verify.**
|
||||
```bash
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && docker compose -f docker-compose.yml -f docker-compose.static.yml ps'
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && docker compose ps'
|
||||
```
|
||||
Confirm `caddy`, `drasl`, `minecraft`, `mc-backup` are Up. Tail the minecraft
|
||||
log briefly and confirm it reaches `Done` (server ready):
|
||||
Confirm `caddy`, `drasl`, `minecraft`, `mc-backup`, `nmsr`, `uptime-kuma` are
|
||||
Up. Tail the minecraft log briefly and confirm it reaches `Done` (server
|
||||
ready):
|
||||
```bash
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && timeout 120 docker compose -f docker-compose.yml -f docker-compose.static.yml logs -f minecraft' 2>/dev/null | grep -m1 "Done"
|
||||
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && timeout 120 docker compose logs -f minecraft' 2>/dev/null | grep -m1 "Done"
|
||||
```
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Never `git reset --hard` or discard changes on `cochi` without telling the user
|
||||
first — there may be host-local edits.
|
||||
- Volumes (`mc_data`, `drasl_state`) persist across `down`; the world is safe. Do
|
||||
NOT pass `-v`/`--volumes` to `down`.
|
||||
- Volumes (`mc_data`, `drasl_state`, `kuma_data`) persist across `down`; the
|
||||
world and monitors are safe. Do NOT pass `-v`/`--volumes` to `down`.
|
||||
- If a step fails, STOP and surface the exact error + the failing command. Do not
|
||||
improvise recovery on production.
|
||||
- Blessing Skin variant or `airgap` mode changes the compose file set — if the
|
||||
user asks for those, follow `plan/14-deploy.md` instead of the fixed commands
|
||||
above.
|
||||
+12
-35
@@ -1,35 +1,22 @@
|
||||
# Copy to .env and fill in real values
|
||||
|
||||
# Single base domain for the whole stack; every service is a subdomain of this.
|
||||
# Use .lan (NOT .local) — .local is intercepted by mDNS on macOS/Linux and
|
||||
# won't resolve through a normal unicast DNS server.
|
||||
BASE_DOMAIN=ulicraft.lan
|
||||
# The Docker host's LAN IP — every service name resolves here.
|
||||
HOST_LAN_IP=192.168.1.10
|
||||
|
||||
# mDNS toggle. true + BASE_DOMAIN=*.local → an Avahi container publishes the
|
||||
# service names over mDNS (zero-config for macOS/Linux guests; Windows needs
|
||||
# Bonjour). false → use your own party DNS (records: tooling/dns-records.sh).
|
||||
# NOTE: mDNS resolves ONLY *.local; it cannot serve the air-gap upstream-host
|
||||
# spoofs — those always need a real DNS server.
|
||||
ENABLE_MDNS=false
|
||||
# DNS is handled OUTSIDE this repo — point ${BASE_DOMAIN} and every subdomain
|
||||
# (auth. pack. avatar. status. distribution. www.) at the host running nginx.
|
||||
BASE_DOMAIN=ulicraft.net
|
||||
|
||||
RCON_PASSWORD=change-me-to-something-random
|
||||
|
||||
# Caddy host-published port (docker-compose.caddy.yml). Default 80 for the
|
||||
# standalone LAN/air-gap path. When the machine's own nginx fronts caddy
|
||||
# (nginx terminates TLS → reverse-proxies to caddy), set a high port and bind
|
||||
# it to localhost so only nginx reaches it:
|
||||
# CADDY_HTTP_PORT=8880
|
||||
# CADDY_HTTP_BIND=127.0.0.1
|
||||
# Then render nginx/ulicraft-caddy.conf.tmpl with CADDY_HTTP_PORT.
|
||||
CADDY_HTTP_PORT=80
|
||||
CADDY_HTTP_BIND=0.0.0.0
|
||||
# caddy host-published port. The host's own nginx terminates TLS and
|
||||
# reverse-proxies to caddy by Host header (nginx/ulicraft-caddy.conf.tmpl), so
|
||||
# bind caddy to a high localhost-only port — only nginx should reach it.
|
||||
CADDY_HTTP_PORT=8880
|
||||
CADDY_HTTP_BIND=127.0.0.1
|
||||
|
||||
# distribution.${BASE_DOMAIN} static vhost (docker-compose.distribution.yml).
|
||||
# Absolute host path to the built static site — it lives in ANOTHER repo.
|
||||
# Bind-mounted read-only into caddy and served at distribution.${BASE_DOMAIN}.
|
||||
# distribution.${BASE_DOMAIN} static vhost. Absolute host path to the built
|
||||
# static site — it lives in ANOTHER repo. Bind-mounted read-only into caddy.
|
||||
DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distribution/dist
|
||||
|
||||
# Only needed if using CurseForge-exclusive mods:
|
||||
# CF_API_KEY=your-curseforge-api-key
|
||||
|
||||
@@ -37,7 +24,7 @@ DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distri
|
||||
# Only needed to issue real TLS certs. DNS-01 needs no inbound ports.
|
||||
LE_EMAIL=you@example.com
|
||||
# space-separated subdomains; apex ${BASE_DOMAIN} is always included
|
||||
LE_SUBDOMAINS=auth pack distribution
|
||||
LE_SUBDOMAINS=auth pack distribution www avatar status
|
||||
# OVH API creds (DNS zone write). Create at https://eu.api.ovh.com/createToken/
|
||||
# OVH_CK can be blank on first run — acme.sh prints an auth URL, then paste it.
|
||||
OVH_END_POINT=ovh-eu
|
||||
@@ -45,13 +32,3 @@ OVH_AK=
|
||||
OVH_AS=
|
||||
OVH_CK=
|
||||
# LE_STAGING=1 # use the untrusted staging CA for dry runs
|
||||
|
||||
# ── Blessing Skin auth variant (docker-compose.blessingskin.yml) ──────
|
||||
# Only needed when running the Blessing Skin override instead of Drasl.
|
||||
BS_DB_NAME=blessingskin
|
||||
BS_DB_USER=blessingskin
|
||||
BS_DB_PASSWORD=change-me-bs-db
|
||||
BS_DB_ROOT_PASSWORD=change-me-bs-root
|
||||
# Generate once and paste here so it survives container recreates:
|
||||
# docker run --rm azusamikan/blessing-skin-server-docker:latest php artisan key:generate --show
|
||||
BS_APP_KEY=base64:replace-with-generated-key
|
||||
+3
-6
@@ -5,15 +5,15 @@ runtime/authlib-injector.jar
|
||||
|
||||
# rendered configs (from tooling/render-config.sh)
|
||||
drasl/config/config.toml
|
||||
dnsmasq/dnsmasq.conf
|
||||
nmsr/config.toml
|
||||
# packwiz pack files rendered from pack/**/*.tmpl by tooling/render-pack.sh
|
||||
# (domain-dependent build output); source of truth is the .tmpl + custom/ jars.
|
||||
pack/mods/*.pw.toml
|
||||
pack/index.toml
|
||||
pack/CustomSkinLoader/CustomSkinLoader.json
|
||||
|
||||
# vendored / mirrored binaries
|
||||
mirror/
|
||||
# vendored binaries
|
||||
launcher/
|
||||
pack/mods-files/
|
||||
|
||||
# landing page: generated build output + deps
|
||||
@@ -22,8 +22,5 @@ landing/node_modules/
|
||||
landing/.astro/
|
||||
landing/dist/
|
||||
|
||||
# exported Caddy CA (per-deploy artifact)
|
||||
caddy/caddy-root-ca.crt
|
||||
|
||||
# Let's Encrypt certs + private keys (issued by tooling/issue-letsencrypt.sh)
|
||||
certs/
|
||||
@@ -1,280 +1,143 @@
|
||||
# Minecraft LAN Party Server — Project Context
|
||||
# Ulicraft Server — Project Context
|
||||
|
||||
> Self-hosted modded Minecraft server for a LAN party (~8 players), with
|
||||
> persistent auth/skin infrastructure designed to outlive the LAN weekend
|
||||
> and integrate with the existing homelab.
|
||||
> Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
|
||||
> (Drasl), a packwiz modpack, avatar rendering, a guest landing page, and uptime
|
||||
> monitoring. Public, internet-present deployment behind the host's nginx + TLS.
|
||||
|
||||
**`plan/` is the source of truth for the design.** Start at `plan/00-overview.md`.
|
||||
This file is a fast orientation + the durable decisions/gotchas; when it disagrees
|
||||
with `plan/` or the code, those win.
|
||||
|
||||
## Goals & Constraints
|
||||
|
||||
- **Players**: 4–10 friends, LAN-based
|
||||
- **Players**: 4–10 friends
|
||||
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
|
||||
- **Pack approach**: Manually curated (~50–100 mods), NOT a forked existing pack
|
||||
- **Minecraft version**: 1.21.1
|
||||
- **Mod loader**: NeoForge (NOT classic Forge — see Decisions below)
|
||||
- **Network**: LAN party context, but stack is persistent (lives past the weekend)
|
||||
- **Auth**: Self-hosted Yggdrasil-compatible (Drasl) + Keycloak OIDC
|
||||
- **Skins**: Handled by Drasl
|
||||
- **Persistence horizon**: Long-term homelab service, not disposable
|
||||
|
||||
## Existing Homelab Context (relevant to this project)
|
||||
|
||||
- **Keycloak 26.0.7** running in Docker Compose (PostgreSQL 17, production mode,
|
||||
bind-mounted secrets, xforwarded proxy headers). This is the IdP for everything.
|
||||
- **NetBird** as the proxy/networking layer for remote access (NOT used for the
|
||||
LAN party itself — see Decisions).
|
||||
- **AdGuard Home** as recursive DNS resolver — used here for `*.home.local` LAN records.
|
||||
- **Multi-VPS topology**: this Minecraft project likely runs on a local box or
|
||||
beefier homelab node, not a VPS (RAM/CPU requirements).
|
||||
- Operator timezone: Europe/Madrid.
|
||||
|
||||
## Key Decisions (with reasoning)
|
||||
|
||||
### NeoForge over classic Forge
|
||||
Despite the user originally saying "Forge", the 1.21.x kitchen-sink ecosystem
|
||||
(ATM10, Leaking Kitchen Sink, etc.) is overwhelmingly NeoForge in 2025/2026.
|
||||
The original Forge team essentially migrated to NeoForge. `itzg/minecraft-server`
|
||||
supports both via `TYPE=NEOFORGE`. Going with NeoForge 1.21.1.
|
||||
|
||||
### Drasl over Ely.by or vanilla offline mode
|
||||
- **Ely.by**: not self-hosted, Russian-hosted, no control over identity layer.
|
||||
- **Vanilla offline + no auth server**: works for one weekend but skins break,
|
||||
no persistent identity, doesn't match homelab philosophy.
|
||||
- **Drasl**: self-hosted Go service, Yggdrasil-compatible, drop-in Mojang
|
||||
replacement, supports skins + capes, **has first-class OIDC support with
|
||||
PKCE** (perfect for Keycloak), actively maintained (current version 3.4.2+).
|
||||
GPLv3 licensed.
|
||||
|
||||
### LAN (not NetBird) for the party itself
|
||||
NetBird is the homelab's remote access layer, but adding a mesh VPN on top
|
||||
of a LAN party introduces complexity for guests with no upside. Drasl and
|
||||
Minecraft live on the LAN; clients reach them via `drasl.home.local` resolved
|
||||
through AdGuard.
|
||||
|
||||
### Manual curation (user choice — flagged as expensive)
|
||||
User insisted on manual curation despite my pushback that forking ATM10 or
|
||||
Leaking Kitchen Sink would save weeks of crash-log triage. **Note for future
|
||||
sessions**: if curation gets painful, the fork-and-trim option is still on
|
||||
the table — Leaking Kitchen Sink is the closest match to the requested vibe.
|
||||
|
||||
### itzg/minecraft-server as base image
|
||||
The de facto standard. Handles NeoForge installation automatically via
|
||||
`TYPE=NEOFORGE` + `VERSION` + `NEOFORGE_VERSION`. Supports Modrinth and
|
||||
CurseForge mod auto-download via `MODRINTH_PROJECTS` and `CURSEFORGE_FILES`.
|
||||
- **Minecraft version**: 1.21.1, **NeoForge** (not classic Forge)
|
||||
- **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC
|
||||
- **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host
|
||||
- **Persistence horizon**: long-term service, not disposable
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
LAN segment
|
||||
│
|
||||
├── AdGuard Home → resolves drasl.home.local, keycloak.home.local
|
||||
│ to the Docker host's LAN IP
|
||||
│
|
||||
├── Keycloak (existing, separate compose)
|
||||
│ realm: homelab
|
||||
│ client: drasl (confidential, PKCE S256)
|
||||
│
|
||||
└── Minecraft host (this project's compose)
|
||||
│
|
||||
├── drasl :25585 (Yggdrasil API + web UI)
|
||||
│ └── OIDC → Keycloak
|
||||
│
|
||||
├── minecraft :25565 (server)
|
||||
│ :24454/udp (Simple Voice Chat, optional)
|
||||
│ └── -javaagent:authlib-injector.jar=http://drasl.home.local:25585/authlib-injector
|
||||
│ └── ONLINE_MODE=false (required for authlib-injector)
|
||||
│
|
||||
└── mc-backup (itzg/mc-backup, 6h interval)
|
||||
└── RCON → minecraft
|
||||
One unified `docker-compose.yml`. The host's own nginx terminates TLS (Let's
|
||||
Encrypt) and reverse-proxies every vhost to caddy (published localhost-only) by
|
||||
Host header. caddy is the internal router; containers reach the stack's own names
|
||||
via caddy's `mcnet` aliases (`auth.`, `pack.`).
|
||||
|
||||
Clients (LAN party guests):
|
||||
Prism Launcher → authlib-injector account pointing at Drasl
|
||||
→ joins minecraft:25565
|
||||
```
|
||||
Internet ── host nginx (TLS, LE certs, HSTS) ──► caddy (127.0.0.1:8880)
|
||||
│ routes by Host:
|
||||
${BASE_DOMAIN} ─► /srv/www landing + /launcher/ downloads
|
||||
auth.${BASE_DOMAIN} ─► drasl (Yggdrasil API + web UI)
|
||||
pack.${BASE_DOMAIN} ─► packwiz files + /custom/ jars
|
||||
avatar.${BASE_DOMAIN} ─► nmsr (skin/avatar renderer, Drasl-backed)
|
||||
status.${BASE_DOMAIN} ─► uptime-kuma (status page + monitors)
|
||||
distribution.${BASE} ─► static site from ANOTHER repo (DISTRIBUTION_WEB_ROOT)
|
||||
|
||||
minecraft :25565 (+ 24454/udp Simple Voice Chat)
|
||||
└ ONLINE_MODE=false, -javaagent authlib-injector → http://auth.${BASE_DOMAIN}/authlib-injector
|
||||
└ mods via PACKWIZ_URL → http://pack.${BASE_DOMAIN}/pack.toml
|
||||
mc-backup ── RCON → minecraft (6h interval, prune 14d, world-only)
|
||||
```
|
||||
|
||||
## Auth Flow (end-to-end)
|
||||
Bring-up: render configs → build landing → fetch launcher → `docker compose up -d --build`.
|
||||
See `plan/12-build-order.md` and `plan/14-deploy.md`. Deploy to prod host `cochi`
|
||||
via the `deploy` skill.
|
||||
|
||||
1. Guest opens `http://drasl.home.local:25585` in browser.
|
||||
2. Clicks "Register with Keycloak" → redirected to Keycloak login.
|
||||
3. Logs in with homelab Keycloak credentials.
|
||||
4. Returned to Drasl, picks a player name, uploads a skin.
|
||||
5. Drasl shows them a "Minecraft Token" on their profile page.
|
||||
6. In Prism Launcher: Settings → Accounts → Add authlib-injector account
|
||||
with URL `http://drasl.home.local:25585/authlib-injector` and the
|
||||
Minecraft Token as password.
|
||||
7. Joins Minecraft server. Server validates session against Drasl (via
|
||||
authlib-injector JVM agent), Drasl confirms, player enters with their skin.
|
||||
## Key Decisions
|
||||
|
||||
- **NeoForge over Forge** — the 1.21.x kitchen-sink ecosystem is overwhelmingly
|
||||
NeoForge; the Forge team migrated. `itzg/minecraft-server` via `TYPE=NEOFORGE`.
|
||||
- **Drasl over Ely.by / vanilla offline** — self-hosted Go service, Yggdrasil
|
||||
drop-in, skins + capes, actively maintained. Run with **password login**
|
||||
(no OIDC/Keycloak in this stack).
|
||||
- **Manual curation** (user choice, flagged expensive) — if it gets painful,
|
||||
fork-and-trim Leaking Kitchen Sink is still on the table (closest vibe match).
|
||||
- **itzg/minecraft-server** base image — de-facto standard, auto-installs NeoForge.
|
||||
- **packwiz** for the modpack — `PACKWIZ_URL` drives server mod install; clients
|
||||
import the same `pack.toml`. Jars come from the upstream CDN per `.pw.toml`.
|
||||
|
||||
## Critical Gotchas
|
||||
|
||||
### Minecraft 1.21+ secure profile incompatibility
|
||||
- Server: must set `enforce-secure-profile=false` (compose: `ENFORCE_SECURE_PROFILE=FALSE`)
|
||||
- Drasl: must set `SignPublicKeys = false`
|
||||
- These are linked. The Drasl docs explicitly warn: *"Mixed authentication does
|
||||
not work with `SignPublicKeys = true` on Minecraft 1.21+."*
|
||||
### Minecraft 1.21+ secure profile
|
||||
- Server: `ENFORCE_SECURE_PROFILE=FALSE` (`enforce-secure-profile=false`)
|
||||
- Drasl: `SignPublicKeys = false`
|
||||
- Linked. Drasl docs: *"Mixed authentication does not work with
|
||||
`SignPublicKeys = true` on Minecraft 1.21+."*
|
||||
|
||||
### authlib-injector JVM agent
|
||||
- Syntax: `-javaagent:/path/to/authlib-injector.jar=<yggdrasil-api-url>`
|
||||
- The URL after `=` is the **API root**, which for Drasl is
|
||||
`<BaseURL>/authlib-injector`.
|
||||
- **Must match between server and client.** If server uses
|
||||
`http://drasl.home.local:25585/authlib-injector` and client uses anything
|
||||
else (e.g. an IP), session validation fails silently with "Invalid session".
|
||||
- Download from: https://github.com/yushijinhun/authlib-injector/releases
|
||||
- Mount into the itzg container at `/extras/authlib-injector.jar`.
|
||||
|
||||
### Keycloak reachability on LAN day
|
||||
If Keycloak is only reachable via NetBird or only from the public internet,
|
||||
**Drasl OIDC registration breaks on LAN-only day**. Options:
|
||||
1. Expose Keycloak on the LAN (simplest)
|
||||
2. Pre-register all guests before the party
|
||||
3. Temporarily disable OIDC and allow password registration during the LAN
|
||||
(`AllowPasswordLogin = true`, comment out the OIDC block)
|
||||
|
||||
### HTTP vs HTTPS for OIDC
|
||||
Modern browsers warn on plain HTTP for OIDC flows but they work. For a
|
||||
production-grade setup, front Drasl with Caddy or Traefik using a local CA
|
||||
cert (smallstep/step-ca pairs well with the existing homelab). For LAN-only
|
||||
HTTP is fine.
|
||||
- Syntax: `-javaagent:/extras/authlib-injector.jar=<yggdrasil-api-url>`
|
||||
- URL after `=` is the **API root** = `<BaseURL>/authlib-injector`. For this
|
||||
stack: `http://auth.${BASE_DOMAIN}/authlib-injector` (internal, over mcnet).
|
||||
- **Must match between server and client** (clients use the public
|
||||
`https://auth.${BASE_DOMAIN}/authlib-injector`), else session validation fails
|
||||
silently with "Invalid session".
|
||||
- Releases: https://github.com/yushijinhun/authlib-injector — jar lives in
|
||||
`runtime/` (mounted at `/extras/authlib-injector.jar`).
|
||||
|
||||
### NeoForge version pinning
|
||||
Mods are picky about exact NeoForge minor versions. The compose currently
|
||||
pins `NEOFORGE_VERSION: "21.1.209"` as a placeholder. **Verify against
|
||||
https://projects.neoforged.net/neoforged/neoforge before deploying.** Don't
|
||||
use `"latest"` for a stable server.
|
||||
|
||||
### Drasl username vs player name
|
||||
- **Drasl username** = OIDC user's email (used for web UI login)
|
||||
- **Player name** = `preferred_username` from OIDC, or user-chosen if
|
||||
`AllowChoosingPlayerName = true`
|
||||
- Keycloak users must have email set. Verify the realm's email-as-username
|
||||
setting and the `preferred_username` mapper are configured.
|
||||
Mods are picky about exact minor versions. Compose pins `NEOFORGE_VERSION:
|
||||
"21.1.209"`. **Verify against https://projects.neoforged.net/neoforged/neoforge
|
||||
before deploying.** Never `"latest"` for a stable server.
|
||||
|
||||
### Mod source preference
|
||||
Prefer **Modrinth over CurseForge** for itzg auto-download. CurseForge
|
||||
requires an API key (`CF_API_KEY`) and has been historically flakier with
|
||||
the itzg image. Use CF only for mods that are exclusive to it.
|
||||
|
||||
## Compose Stack
|
||||
|
||||
The working compose file is at `./docker-compose.yml`. Key environment vars
|
||||
documented inline. Adjacent files:
|
||||
|
||||
```
|
||||
.
|
||||
├── docker-compose.yml
|
||||
├── .env # RCON_PASSWORD, CF_API_KEY (if needed)
|
||||
├── drasl/
|
||||
│ └── config/
|
||||
│ ├── config.toml # see ./drasl-config.toml example
|
||||
│ └── keycloak-client-secret # single line, no trailing newline
|
||||
├── extras/
|
||||
│ ├── authlib-injector.jar # downloaded from yushijinhun's releases
|
||||
│ ├── modrinth-mods.txt # one mod slug per line
|
||||
│ └── cf-mods.txt # only if using CurseForge mods
|
||||
├── backups/ # mc-backup writes here
|
||||
└── CLAUDE.md # this file
|
||||
```
|
||||
Prefer **Modrinth over CurseForge** (`packwiz modrinth add <slug>`). CF needs
|
||||
`CF_API_KEY` and is flakier. Tag client-only mods `side = "client"` so the server
|
||||
doesn't pull and crash on them.
|
||||
|
||||
### Memory sizing
|
||||
With ~50–100 mods and 8 concurrent players on 1.21.1:
|
||||
- `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G` is a safe starting point
|
||||
- Use `USE_AIKAR_FLAGS: TRUE` for the well-known GC tuning
|
||||
- Monitor with `mc-monitor` (itzg makes one) if you want metrics
|
||||
~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
|
||||
`USE_AIKAR_FLAGS: TRUE`.
|
||||
|
||||
### Backups
|
||||
`itzg/mc-backup` runs alongside, talks to the server via RCON, takes
|
||||
snapshots every 6h, prunes after 14 days. Backups are world-only (no mod
|
||||
jars), which is correct — mods are reproducible from the mod list files.
|
||||
## Config (.env)
|
||||
|
||||
## Keycloak Client Configuration
|
||||
`BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT`/`CADDY_HTTP_BIND`,
|
||||
`DISTRIBUTION_WEB_ROOT`, the Let's Encrypt block (`LE_EMAIL`, `LE_SUBDOMAINS`,
|
||||
`OVH_*`), optional `CF_API_KEY`. Rendered configs (drasl, nmsr, packwiz) come
|
||||
from `*.tmpl` via `tooling/render-config.sh` (substitutes `${BASE_DOMAIN}` only).
|
||||
|
||||
In the `homelab` realm (or whatever the realm is called):
|
||||
## Client Distribution (guests)
|
||||
|
||||
1. **Clients → Create client**
|
||||
- Client type: `OpenID Connect`
|
||||
- Client ID: `drasl`
|
||||
- Name: `Drasl Minecraft Auth`
|
||||
|
||||
2. **Capability config**
|
||||
- Client authentication: **ON** (confidential client)
|
||||
- Authentication flow: **Standard flow** only
|
||||
- Disable: Direct access grants, Implicit, Service accounts
|
||||
|
||||
3. **Login settings**
|
||||
- Root URL: `http://drasl.home.local:25585`
|
||||
- Valid redirect URIs: `http://drasl.home.local:25585/web/oidc-callback/Keycloak`
|
||||
(the `Keycloak` at the end MUST match the `Name = "Keycloak"` in Drasl's
|
||||
`[[RegistrationOIDC]]` block — case sensitive)
|
||||
- Web origins: `http://drasl.home.local:25585`
|
||||
|
||||
4. **Credentials tab** → copy Client Secret to
|
||||
`./drasl/config/keycloak-client-secret` (no trailing newline).
|
||||
|
||||
5. **Advanced tab** → PKCE Code Challenge Method: `S256`
|
||||
|
||||
## Client Distribution (LAN guests)
|
||||
|
||||
### Recommended launcher
|
||||
**Prism Launcher** — open source, cross-platform (Win/Mac/Linux), first-class
|
||||
authlib-injector support, can import/export instance ZIPs.
|
||||
|
||||
### Per-guest one-time setup
|
||||
1. Install Prism Launcher.
|
||||
2. Add authlib-injector account:
|
||||
- URL: `http://drasl.home.local:25585/authlib-injector`
|
||||
- Username/password: their Drasl credentials (or Minecraft Token if
|
||||
registered via Keycloak OIDC)
|
||||
3. Import the modpack instance ZIP.
|
||||
|
||||
### What to ship guests
|
||||
- A Prism instance ZIP (right-click instance → Export Instance)
|
||||
- A one-page README with the auth URL and import steps
|
||||
- Optionally `authlib-injector.jar` for guests who refuse Prism
|
||||
Launcher: **FjordLauncherUnlocked** (Prism fork, first-class authlib-injector),
|
||||
mirrored at apex `/launcher/`. Per guest:
|
||||
1. Download from `https://${BASE_DOMAIN}` → `/launcher/`.
|
||||
2. Add an authlib-injector account, URL
|
||||
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
|
||||
`https://auth.${BASE_DOMAIN}`).
|
||||
3. Import the pack `https://pack.${BASE_DOMAIN}/pack.toml`.
|
||||
4. Connect to `${BASE_DOMAIN}` (`:25565`).
|
||||
|
||||
## Open / Pending Work
|
||||
|
||||
These were on the roadmap when we ended the brainstorm:
|
||||
|
||||
- [ ] **Mod shortlist for the kitchen-sink pack.** Categories to fill:
|
||||
- Performance (Embeddium/Sodium-equivalent for NeoForge, FerriteCore, ModernFix)
|
||||
- Tech/automation (Mekanism, Create, Immersive Engineering, AE2)
|
||||
- Magic (Ars Nouveau, Botania, Iron's Spells 'n Spellbooks)
|
||||
- Storage (Sophisticated Storage/Backpacks, Functional Storage)
|
||||
- Exploration (YUNG's structures, Repurposed Structures, biome packs)
|
||||
- QoL (JEI, Jade, JEI Resources, Inventory Profiles Next, AppleSkin)
|
||||
- World gen (Tectonic, Terralith — check NeoForge 1.21.1 compat)
|
||||
- Compat glue (Polymorph for recipe conflicts)
|
||||
- Map (XaeroMinimap + Xaero World Map)
|
||||
- Voice (Simple Voice Chat — port 24454/udp already in compose)
|
||||
- [ ] **Decide on dimension/server-side performance mods** (C2ME, Lithium-equivalent)
|
||||
- [ ] **Reverse proxy + local TLS** for Drasl (Caddy + step-ca recommended,
|
||||
given the existing homelab)
|
||||
- [ ] **DNS record** in AdGuard for `drasl.home.local` → Docker host LAN IP
|
||||
- [ ] **Keycloak client** creation per the section above
|
||||
- [ ] **Verify NeoForge version** against current stable before first deploy
|
||||
- [ ] **Bootstrap admin in Drasl**: leave `AllowPasswordLogin = true` initially,
|
||||
create the admin account, then optionally disable password login to force OIDC
|
||||
- [ ] **Mod shortlist** for the kitchen-sink pack: Performance (Embeddium,
|
||||
FerriteCore, ModernFix), Tech (Mekanism, Create, IE, AE2), Magic (Ars Nouveau,
|
||||
Botania, Iron's Spells), Storage (Sophisticated, Functional), Exploration
|
||||
(YUNG's, Repurposed Structures), QoL (JEI, Jade, IPN, AppleSkin), World gen
|
||||
(Tectonic, Terralith — verify NeoForge 1.21.1), Compat (Polymorph), Map (Xaero),
|
||||
Voice (Simple Voice Chat — 24454/udp already in compose).
|
||||
- [ ] Server-side perf mods (C2ME, etc.).
|
||||
- [ ] **Verify NeoForge version** against current stable before first deploy.
|
||||
- [ ] **Bootstrap Drasl admin** account.
|
||||
|
||||
## Reference URLs
|
||||
|
||||
- Drasl repo: https://github.com/unmojang/drasl
|
||||
- Drasl configuration docs: https://github.com/unmojang/drasl/blob/master/doc/configuration.md
|
||||
- Drasl recipes (example configs): https://github.com/unmojang/drasl/blob/master/doc/recipes.md
|
||||
- Drasl: https://github.com/unmojang/drasl — config docs:
|
||||
https://github.com/unmojang/drasl/blob/master/doc/configuration.md
|
||||
- authlib-injector: https://github.com/yushijinhun/authlib-injector
|
||||
- itzg/minecraft-server: https://github.com/itzg/docker-minecraft-server
|
||||
- itzg NeoForge docs: https://github.com/itzg/docker-minecraft-server/blob/master/docs/types-and-platforms/server-types/forge.md
|
||||
(NeoForge: …/docs/types-and-platforms/server-types/forge.md)
|
||||
- itzg/mc-backup: https://github.com/itzg/docker-mc-backup
|
||||
- NeoForge versions: https://projects.neoforged.net/neoforged/neoforge
|
||||
- Prism Launcher: https://prismlauncher.org/
|
||||
- Reference modpacks for inspiration (NOT forking):
|
||||
- ATM10 (Modrinth/CurseForge — ~500 mods, NeoForge 1.21.1)
|
||||
- Leaking Kitchen Sink (CurseForge — trimmed ~150 mods, NeoForge 1.21.1)
|
||||
- NMSR: https://github.com/NickAcPT/nmsr-rs
|
||||
- Uptime Kuma: https://github.com/louislam/uptime-kuma
|
||||
- FjordLauncherUnlocked: https://github.com/hero-persson/FjordLauncherUnlocked
|
||||
- Reference packs (inspiration, NOT forking): ATM10, Leaking Kitchen Sink.
|
||||
|
||||
## Communication Preferences (carry-over from past sessions)
|
||||
## Communication Preferences (carry-over)
|
||||
|
||||
- Concise and direct
|
||||
- Diagnose and resolve without demanding detailed reproduction steps
|
||||
- Prefer iterative single-change updates over large rewrites
|
||||
- Cautious scripts that halt on ambiguity rather than proceeding silently
|
||||
- Avoid over-engineered responses when a simple answer suffices
|
||||
- Spanish or English fine; user is comfortable in both
|
||||
- Concise and direct; diagnose + resolve without demanding repro steps.
|
||||
- Prefer iterative single-change updates over large rewrites.
|
||||
- Cautious scripts that halt on ambiguity rather than proceeding silently.
|
||||
- Avoid over-engineering when a simple answer suffices.
|
||||
- Spanish or English both fine.
|
||||
@@ -1,111 +1,159 @@
|
||||
# Ulicraft Server
|
||||
|
||||
Self-hosted modded Minecraft (NeoForge 1.21.1) for a LAN party. Runs **online**
|
||||
(internet present) or **fully air-gapped** (offline). Self-hosted auth/skins
|
||||
(Drasl), a packwiz modpack, a guest landing page, and a transparent mirror of
|
||||
Mojang/launcher/NeoForge assets so guest laptops need no internet.
|
||||
Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
|
||||
(Drasl), a packwiz modpack, avatar rendering, a guest landing page, and uptime
|
||||
monitoring. One unified `docker-compose.yml` runs the whole stack behind the
|
||||
host's nginx + Let's Encrypt.
|
||||
|
||||
## Stack
|
||||
|
||||
One compose file, one `docker compose up -d`:
|
||||
|
||||
| Service | Image | Role |
|
||||
|---|---|---|
|
||||
| `minecraft` | itzg/minecraft-server | NeoForge 1.21.1 server, `:25565` |
|
||||
| `drasl` | unmojang/drasl | Yggdrasil auth + skins (password login) |
|
||||
| `caddy` | caddy:alpine | ingress: landing, auth/packwiz proxy, asset mirror (`tls internal`) |
|
||||
| `caddy` | caddy:alpine | internal ingress: routes every vhost by Host header |
|
||||
| `nmsr` | built from source | skin/avatar renderer at `avatar.${BASE_DOMAIN}` |
|
||||
| `uptime-kuma` | louislam/uptime-kuma | status page at `status.${BASE_DOMAIN}` |
|
||||
| `mc-backup` | itzg/mc-backup | world backups every 6h via RCON |
|
||||
|
||||
Optional layers: `avahi` (mDNS `.local`), `dnsmasq` (turnkey DNS) — only if you
|
||||
don't run your own DNS.
|
||||
Vhosts (all proxied through the host nginx → caddy):
|
||||
|
||||
Config lives in `.env`: `BASE_DOMAIN` (default `ulicraft.lan`), `HOST_LAN_IP`,
|
||||
`RCON_PASSWORD`, `ENABLE_MDNS`. Services are subdomains: `auth.`, `packwiz.`,
|
||||
`mc.`, plus the apex landing page.
|
||||
- apex `${BASE_DOMAIN}` — landing page + `/launcher/` downloads
|
||||
- `auth.` — Drasl
|
||||
- `pack.` — packwiz metadata + `/custom/` jars
|
||||
- `avatar.` — NMSR
|
||||
- `status.` — Uptime Kuma
|
||||
- `distribution.` — static site from another repo (`DISTRIBUTION_WEB_ROOT`)
|
||||
|
||||
## DNS (pick one)
|
||||
Config lives in `.env`: `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT` /
|
||||
`CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`. See `.env.example`.
|
||||
|
||||
Names must resolve to `HOST_LAN_IP`. Use **your own party DNS server** (recommended):
|
||||
## DNS
|
||||
|
||||
```sh
|
||||
tooling/dns-records.sh online # service names only (internet present)
|
||||
tooling/dns-records.sh airgap # + Mojang/launcher/NeoForge spoofs (offline)
|
||||
```
|
||||
Paste the printed records into your DNS. **Use `.lan`, not `.local`** — `.local`
|
||||
is intercepted by mDNS and won't resolve via unicast DNS.
|
||||
|
||||
Alternatives: `ENABLE_MDNS=true` + `BASE_DOMAIN=*.local` (zero-config mDNS,
|
||||
macOS/Linux native, Windows needs Bonjour) — but mDNS can't serve the air-gap
|
||||
upstream spoofs. Or layer in `docker-compose.dnsmasq.yml` for a turnkey DNS.
|
||||
|
||||
### Simulate the DNS via /etc/hosts (single machine, testing)
|
||||
|
||||
No DNS server? Append the records straight to `/etc/hosts` (marker-wrapped):
|
||||
|
||||
```sh
|
||||
tooling/dns-records.sh online hosts | sudo tee -a /etc/hosts # services only
|
||||
tooling/dns-records.sh airgap hosts | sudo tee -a /etc/hosts # + Mojang spoofs
|
||||
```
|
||||
|
||||
Remove them later:
|
||||
|
||||
```sh
|
||||
sudo sed -i '/# >>> ulicraft/,/# <<< ulicraft/d' /etc/hosts
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `/etc/hosts` has **no wildcard** — only the listed names resolve.
|
||||
- `airgap` adds the Mojang/launcher/NeoForge hostnames pointing at the LAN; this
|
||||
**breaks normal internet access to those domains** while present. Use it only
|
||||
while offline; for an online box use `online`.
|
||||
- `mc.<domain>` works (client defaults to :25565); SRV isn't used via `/etc/hosts`.
|
||||
Handled **outside this repo**. Point `${BASE_DOMAIN}` and every subdomain
|
||||
(`auth. pack. avatar. status. distribution. www.`) at the host running nginx.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Docker + Docker Compose; for prep also `pnpm`, `packwiz`, `jq`, `curl`,
|
||||
`envsubst`, `sha1sum`.
|
||||
`envsubst`.
|
||||
|
||||
## Launch
|
||||
|
||||
```sh
|
||||
cp .env.example .env # set BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD
|
||||
cp .env.example .env # set BASE_DOMAIN, RCON_PASSWORD, DISTRIBUTION_WEB_ROOT
|
||||
|
||||
tooling/build-stack.sh --prep # ONLINE, once: render, build landing,
|
||||
# mirror mods+assets, fetch launcher +
|
||||
# authlib, pre-bake the server volume
|
||||
# One-time / on change — render configs + build content:
|
||||
tooling/render-config.sh # drasl + nmsr + packwiz configs from *.tmpl
|
||||
( cd landing && pnpm install && BASE_DOMAIN="$BASE_DOMAIN" pnpm run build ) # → www/
|
||||
tooling/fetch-launcher.sh # FjordLauncher assets → launcher/ (served at /launcher/)
|
||||
tooling/fetch-authlib.sh # authlib-injector.jar → runtime/ (server JVM agent)
|
||||
|
||||
tooling/build-stack.sh --up online # internet present, no mirror
|
||||
tooling/build-stack.sh --up airgap # offline; full asset mirror on :443 (default)
|
||||
tooling/build-stack.sh --up core # base only (debugging)
|
||||
docker compose up -d --build # first run installs NeoForge + mods, builds nmsr
|
||||
docker compose down # stop
|
||||
```
|
||||
|
||||
`--up` prints the DNS records to add for that mode. Down:
|
||||
`docker compose -f docker-compose.yml -f docker-compose.static.yml -f docker-compose.mirror.yml down`
|
||||
First boot: itzg installs NeoForge + the packwiz mods into the `mc_data` volume;
|
||||
nmsr does a one-time Rust compile. Watch `docker compose logs -f minecraft` until
|
||||
`Done`.
|
||||
|
||||
## Public TLS (host nginx in front of caddy)
|
||||
|
||||
caddy is published on a localhost-only port (`CADDY_HTTP_BIND=127.0.0.1`,
|
||||
`CADDY_HTTP_PORT=8880`). The host's own nginx terminates TLS with Let's Encrypt
|
||||
certs and reverse-proxies every vhost to caddy by Host header. HTTPS-only: HTTP
|
||||
301s to HTTPS and every TLS vhost sends HSTS.
|
||||
|
||||
1. **Issue certs** (OVH DNS-01, no inbound ports needed):
|
||||
```sh
|
||||
# .env: BASE_DOMAIN, LE_EMAIL, OVH_* creds,
|
||||
# LE_SUBDOMAINS="auth pack distribution www avatar status"
|
||||
tooling/issue-letsencrypt.sh # → certs/<name>/{cert,key}.pem
|
||||
```
|
||||
|
||||
2. **Render + install the nginx vhost** with `tooling/render-nginx.sh`. It pulls
|
||||
`BASE_DOMAIN` + `CADDY_HTTP_PORT` from `.env`, defaults `APP_DIR` to the repo
|
||||
checkout (where `certs/` live), and renders `nginx/ulicraft-caddy.conf.tmpl`:
|
||||
```sh
|
||||
tooling/render-nginx.sh # preview to stdout (dry run)
|
||||
tooling/render-nginx.sh --install # write, symlink, nginx -t, reload
|
||||
```
|
||||
Override `APP_DIR=/path` if the repo isn't at the cert location. Re-run
|
||||
whenever you add a subdomain so its server block is rendered.
|
||||
|
||||
## Avatar renderer (NMSR)
|
||||
|
||||
`avatar.${BASE_DOMAIN}` renders players' skins/avatars via
|
||||
[NMSR-aas](https://github.com/NickAcPT/nmsr-rs), sourced from **Drasl** (not
|
||||
Mojang). No upstream image exists, so it's built from source — `docker/nmsr/`
|
||||
pins a commit; bump `ARG NMSR_REF` to update.
|
||||
|
||||
- **Config**: `nmsr/config.toml.tmpl` → rendered to `nmsr/config.toml` by
|
||||
`tooling/render-config.sh`. `[mojank]` points every Mojang endpoint at
|
||||
`http://auth.${BASE_DOMAIN}`; Drasl serves the Mojang-compatible routes at its
|
||||
bare BaseURL and embeds absolute skin URLs that NMSR fetches directly.
|
||||
`allow_offline_mode_uuids = true` (Drasl issues offline v3 UUIDs).
|
||||
- **Network**: skin resolution stays internal over `mcnet` (caddy alias
|
||||
`auth.${BASE_DOMAIN}` → drasl). caddy reverse-proxies `avatar.` → `nmsr:8080`.
|
||||
- **Endpoints**: e.g. `https://avatar.${BASE_DOMAIN}/skin/<player>`,
|
||||
`/face/<player>`, `/fullbody/<player>` — by username or UUID.
|
||||
|
||||
First build is heavy (Rust compile). Headless rendering uses lavapipe (software
|
||||
Vulkan); if renders fail, check `docker logs nmsr` for Vulkan device init.
|
||||
|
||||
## Status monitoring (Uptime Kuma)
|
||||
|
||||
`status.${BASE_DOMAIN}` runs [Uptime Kuma](https://github.com/louislam/uptime-kuma)
|
||||
— uptime probes for every vhost **and** a native Minecraft-protocol ping (player
|
||||
count + up/down), plus a public status page. It joins `mcnet`, so monitors can
|
||||
probe the stack by internal service name. caddy reverse-proxies `status.` →
|
||||
`uptime-kuma:3001`.
|
||||
|
||||
First run, open `https://status.${BASE_DOMAIN}`, create the admin account, and
|
||||
add monitors. Kuma has no config-as-code — add these once via the UI (data
|
||||
persists in the `kuma_data` volume):
|
||||
|
||||
| Monitor | Type | Target |
|
||||
|---|---|---|
|
||||
| Minecraft | Minecraft Server | `minecraft` : `25565` |
|
||||
| Drasl auth | HTTP(s) | `https://auth.${BASE_DOMAIN}` |
|
||||
| Landing (apex) | HTTP(s) | `https://${BASE_DOMAIN}` |
|
||||
| Packwiz | HTTP(s) | `https://pack.${BASE_DOMAIN}` |
|
||||
| Distribution | HTTP(s) | `https://distribution.${BASE_DOMAIN}` |
|
||||
| Avatar (NMSR) | HTTP(s) | `https://avatar.${BASE_DOMAIN}` |
|
||||
|
||||
Internal-name targets (`minecraft`, `drasl:25585`, `nmsr:8080`) isolate "service
|
||||
down" from "ingress/TLS/DNS down"; public-URL targets test the whole chain.
|
||||
`status` is in the default `LE_SUBDOMAINS`; the nginx vhost adds websocket
|
||||
upgrade headers for Kuma's live UI.
|
||||
|
||||
## Join (guests)
|
||||
|
||||
1. Open `http://ulicraft.lan`, download FjordLauncherUnlocked for your OS.
|
||||
2. Add an **authlib-injector** account, URL `http://auth.ulicraft.lan/authlib-injector`
|
||||
(register/login at `http://auth.ulicraft.lan`).
|
||||
3. **Air-gap only:** trust the local CA — download `http://ulicraft.lan/ca.crt`
|
||||
and add it to your OS trust store (so the HTTPS asset mirror is trusted).
|
||||
4. Import the pack: `http://packwiz.ulicraft.lan/pack.toml`.
|
||||
5. Connect to **`mc.ulicraft.lan`** (no port needed).
|
||||
1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS.
|
||||
2. Add an **authlib-injector** account, URL
|
||||
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
|
||||
`https://auth.${BASE_DOMAIN}`).
|
||||
3. Import the pack: `https://pack.${BASE_DOMAIN}/pack.toml`.
|
||||
4. Connect to **`${BASE_DOMAIN}`** (Minecraft, `:25565`).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
docker-compose.yml # core: drasl, minecraft, mc-backup, caddy
|
||||
.static.yml .mirror.yml # online/airgap layers
|
||||
.avahi.yml .dnsmasq.yml # optional DNS layers
|
||||
caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets (core/static/mirror)
|
||||
docker-compose.yml # the whole stack (one file)
|
||||
caddy/Caddyfile + conf.d/*.caddy # ingress vhost snippets
|
||||
# (core/static/distribution/avatar/status)
|
||||
drasl/config/config.toml.tmpl # auth config (rendered)
|
||||
dnsmasq/dnsmasq.conf.tmpl # optional DNS config (rendered)
|
||||
docker/avahi/ # mDNS responder image
|
||||
nmsr/config.toml.tmpl # avatar renderer config, Drasl-backed (rendered)
|
||||
docker/nmsr/ # built-from-source NMSR image
|
||||
pack/ # packwiz modpack source
|
||||
landing/ # Astro site → www/
|
||||
tooling/ # build-stack, render-config, dns-records,
|
||||
# mirror-mods, mirror-airgap, fetch-launcher
|
||||
mirror/ # vendored jars + launcher + asset mirror (gitignored)
|
||||
plan/ # full design docs (00–12)
|
||||
launcher/ # FjordLauncher assets (gitignored)
|
||||
tooling/ # render-config, render-pack, render-nginx,
|
||||
# issue-letsencrypt, fetch-launcher, add-custom-mod
|
||||
nginx/ulicraft-caddy.conf.tmpl # host-nginx TLS vhost template (front of caddy)
|
||||
plan/ # design docs
|
||||
```
|
||||
|
||||
See `plan/00-overview.md` for the full design.
|
||||
+9
-7
@@ -1,10 +1,12 @@
|
||||
# Base Caddy config. Vhosts live in conf.d/*.caddy snippets that are mounted
|
||||
# in selectively so the stack can run with or without the static site / mirror:
|
||||
# conf.d/00-core.caddy auth + packwiz (always — core ingress)
|
||||
# conf.d/10-static.caddy apex landing + launcher downloads (static toggle)
|
||||
# conf.d/20-mirror.caddy air-gap upstream mirror (tls internal) (mirror toggle)
|
||||
# auto_https disable_redirects: http:// sites stay plaintext on :80; the mirror
|
||||
# snippet's hosts still get internal TLS on :443 when that snippet is present.
|
||||
# Base Caddy config. Vhosts live in conf.d/*.caddy snippets:
|
||||
# conf.d/00-core.caddy auth + packwiz
|
||||
# conf.d/10-static.caddy apex landing + launcher downloads
|
||||
# conf.d/30-distribution.caddy static site from another repo
|
||||
# conf.d/40-avatar.caddy nmsr skin/avatar renderer
|
||||
# conf.d/50-status.caddy uptime-kuma status page
|
||||
# All snippets are plain http:// on :80 — the host's nginx terminates TLS in
|
||||
# front (nginx/ulicraft-caddy.conf.tmpl). disable_redirects keeps caddy from
|
||||
# upgrading to its own https.
|
||||
{
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Core ingress — Blessing Skin variant. Replaces 00-core.caddy when the
|
||||
# docker-compose.blessingskin.yml override is in play (mounted at the same
|
||||
# target path). auth.* now points at Blessing Skin instead of Drasl.
|
||||
#
|
||||
# Blessing Skin serves BOTH its web UI (/) and the Yggdrasil API (/api/yggdrasil)
|
||||
# on the same vhost, so a single reverse_proxy covers everything.
|
||||
http://auth.{$BASE_DOMAIN} {
|
||||
reverse_proxy blessing-skin:80
|
||||
}
|
||||
|
||||
http://pack.{$BASE_DOMAIN} {
|
||||
root * /srv/pack
|
||||
file_server browse
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
# Static toggle — apex landing page + launcher downloads.
|
||||
# Mounted only when the stack runs with static files (build-stack.sh up static|full).
|
||||
# Apex — landing page (/srv/www) + launcher downloads (/srv/launcher).
|
||||
http://{$BASE_DOMAIN} {
|
||||
# Caddy's local CA root, so guests can trust the air-gap HTTPS mirror.
|
||||
# Mounted from ./caddy/caddy-root-ca.crt (exported by build-stack prep;
|
||||
# the live pki dir is root-only 0600 and can't be served directly).
|
||||
handle /ca.crt {
|
||||
root * /srv/ca-pub
|
||||
file_server
|
||||
}
|
||||
# Launcher downloads are a sibling mount (/srv/launcher), not nested under
|
||||
# the read-only /srv/www. handle_path strips the /launcher prefix.
|
||||
handle_path /launcher/* {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Mirror toggle — full client air-gap mirror (see plan/11-full-airgap-mirror.md).
|
||||
# Byte-exact copies of the real upstream hosts over HTTPS with Caddy's local CA.
|
||||
# dnsmasq spoofs these hostnames -> our host; {host} roots each at its subtree.
|
||||
# Mounted only when the stack runs with the mirror (build-stack.sh up mirror|full).
|
||||
meta.prismlauncher.org, piston-meta.mojang.com, piston-data.mojang.com, libraries.minecraft.net, maven.neoforged.net, resources.download.minecraft.net {
|
||||
tls internal
|
||||
root * /srv/mirror/{host}
|
||||
file_server
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
# Distribution toggle — static site served from a web root in ANOTHER repo.
|
||||
# The host path is set by DISTRIBUTION_WEB_ROOT in .env and bind-mounted to
|
||||
# /srv/distribution by docker-compose.caddy.yml. Mounted only when that var is
|
||||
# set (see the distribution override).
|
||||
# Distribution — static site served from a web root in ANOTHER repo. The host
|
||||
# path is set by DISTRIBUTION_WEB_ROOT in .env and bind-mounted to
|
||||
# /srv/distribution by the caddy service in docker-compose.yml.
|
||||
http://distribution.{$BASE_DOMAIN} {
|
||||
root * /srv/distribution
|
||||
file_server browse
|
||||
|
||||
@@ -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
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Single source of config: BASE_DOMAIN + HOST_LAN_IP in .env.
|
||||
# DNS is provided by YOUR party DNS server — see tooling/dns-records.sh for the
|
||||
# records to add (online vs airgap). A turnkey dnsmasq is available as an
|
||||
# optional layer: docker-compose.dnsmasq.yml.
|
||||
# - ingress is layered on, NOT in this base file: caddy (docker-compose.caddy.yml,
|
||||
# LAN/air-gap, holds mcnet aliases) OR the host's nginx (docker-compose.nginx.yml
|
||||
# + Let's Encrypt, production)
|
||||
# - drasl handles auth (password login; NO Keycloak/OIDC in this stack)
|
||||
# - packwiz pack is served by the ingress at pack.${BASE_DOMAIN}
|
||||
# - minecraft installs mods via PACKWIZ_URL, authlib-injector -> auth.
|
||||
# Single, unified compose. One file holds the whole stack:
|
||||
# drasl auth + skins (Yggdrasil), internal only
|
||||
# minecraft the server (itzg/NEOFORGE), installs mods via packwiz
|
||||
# mc-backup world backups every 6h via RCON
|
||||
# caddy internal ingress — routes auth./pack./apex/launcher/avatar.
|
||||
# /distribution. by Host header (conf.d/*.caddy)
|
||||
# nmsr skin/avatar renderer at avatar.${BASE_DOMAIN}
|
||||
# uptime-kuma status monitoring at status.${BASE_DOMAIN}
|
||||
#
|
||||
# Bring up: docker compose up -d (render configs first: tooling/render-config.sh)
|
||||
#
|
||||
# DNS is handled OUTSIDE this repo — point every service name at the host.
|
||||
# Production TLS terminates at the HOST's nginx in front of caddy
|
||||
# (nginx/ulicraft-caddy.conf.tmpl + Let's Encrypt); caddy is published on a
|
||||
# localhost-only port (CADDY_HTTP_BIND/CADDY_HTTP_PORT in .env). Containers reach
|
||||
# the stack's own names internally via caddy's mcnet aliases (auth./pack.).
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
services:
|
||||
# NOTE: the HTTP(S) ingress is NOT in this base file.
|
||||
# - LAN / air-gap: caddy (docker-compose.caddy.yml) — added by build-stack.sh
|
||||
# - production: the HOST's nginx + Let's Encrypt (docker-compose.nginx.yml
|
||||
# publishes drasl to localhost; nginx/ulicraft.conf.tmpl)
|
||||
# Base alone (drasl + minecraft + mc-backup) is not a complete deployment —
|
||||
# always layer one ingress file on top.
|
||||
drasl:
|
||||
image: unmojang/drasl:latest
|
||||
container_name: drasl
|
||||
restart: unless-stopped
|
||||
# Internal-only: no published host port. Reached via Caddy at
|
||||
# auth.${BASE_DOMAIN} (Caddy holds the network alias on mcnet).
|
||||
# Internal-only: no published host port. Reached via caddy at
|
||||
# auth.${BASE_DOMAIN} (caddy holds the network alias on mcnet).
|
||||
volumes:
|
||||
- ./drasl/config:/etc/drasl:ro
|
||||
- drasl_state:/var/lib/drasl
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
|
||||
# ── Auth: offline mode + authlib-injector pointing at Drasl ─
|
||||
ONLINE_MODE: "FALSE"
|
||||
# mount authlib-injector.jar at /extras/authlib-injector.jar
|
||||
# Resolves over mcnet to caddy (alias auth.${BASE_DOMAIN}) -> drasl.
|
||||
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=http://auth.${BASE_DOMAIN}/authlib-injector"
|
||||
|
||||
# ── Server config ───────────────────────────────────────────
|
||||
@@ -71,9 +71,7 @@ services:
|
||||
ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+
|
||||
ALLOW_FLIGHT: "TRUE" # many tech mods need this
|
||||
|
||||
# ── Mod source: packwiz pack served by the ingress ──────────
|
||||
# http + caddy alias by default; the nginx override switches this to
|
||||
# https + host-gateway (extra_hosts) for the production path.
|
||||
# ── Mod source: packwiz pack served by caddy (alias pack.) ──
|
||||
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
|
||||
|
||||
# ── RCON for remote admin ───────────────────────────────────
|
||||
@@ -86,11 +84,17 @@ services:
|
||||
- mc_data:/data
|
||||
- ./runtime:/extras:ro # authlib-injector.jar lives here
|
||||
depends_on:
|
||||
- drasl # ingress (caddy/nginx) is layered separately
|
||||
- drasl
|
||||
- caddy # pack./auth. must resolve at boot
|
||||
# Containers don't inherit the host's /etc/hosts; the LAN resolver returns a
|
||||
# dead address for the public names. Pin auth./pack. to the host so HTTPS to
|
||||
# them lands on the host's nginx (valid LE cert) -> caddy -> service.
|
||||
extra_hosts:
|
||||
- "auth.${BASE_DOMAIN}:host-gateway"
|
||||
- "pack.${BASE_DOMAIN}:host-gateway"
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
# Optional: backups
|
||||
mc-backup:
|
||||
image: itzg/mc-backup
|
||||
container_name: mc-backup
|
||||
@@ -109,9 +113,72 @@ services:
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
# Internal ingress. Routes every vhost by Host header (conf.d/*.caddy):
|
||||
# auth. -> drasl, pack. -> packwiz files, apex -> landing + /launcher,
|
||||
# avatar. -> nmsr, status. -> uptime-kuma, distribution. -> static site.
|
||||
# Published on a localhost-only port; the host's nginx terminates TLS in
|
||||
# front of it (nginx/ulicraft-caddy.conf.tmpl).
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${CADDY_HTTP_BIND:-127.0.0.1}:${CADDY_HTTP_PORT:-8880}:80"
|
||||
environment:
|
||||
BASE_DOMAIN: ${BASE_DOMAIN}
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./caddy/conf.d/00-core.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
||||
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
|
||||
- ./caddy/conf.d/30-distribution.caddy:/etc/caddy/conf.d/30-distribution.caddy:ro
|
||||
- ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro
|
||||
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
|
||||
- ./www:/srv/www:ro
|
||||
- ./launcher:/srv/launcher:ro
|
||||
- ./pack:/srv/pack:ro
|
||||
# Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh
|
||||
# doesn't index them as direct files. Served at pack.${BASE_DOMAIN}/custom/.
|
||||
# Nested under the :ro /srv/pack mount — the mountpoint pack/custom/ must
|
||||
# pre-exist on the host (tracked via pack/custom/.gitkeep) or Docker can't
|
||||
# mkdir it in the read-only parent.
|
||||
- ./custom:/srv/pack/custom:ro
|
||||
# Static site from ANOTHER repo (absolute host path in .env).
|
||||
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}:/srv/distribution:ro
|
||||
networks:
|
||||
mcnet:
|
||||
# Containers resolve the stack's own public names to caddy internally.
|
||||
aliases:
|
||||
- "auth.${BASE_DOMAIN}"
|
||||
- "pack.${BASE_DOMAIN}"
|
||||
|
||||
# Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls
|
||||
# player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080.
|
||||
nmsr:
|
||||
build:
|
||||
context: ./docker/nmsr
|
||||
image: ulicraft/nmsr:local
|
||||
container_name: nmsr
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nmsr/config.toml:/nmsr/config.toml:ro
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
# Status monitoring + public status page. caddy proxies status. -> :3001.
|
||||
# Joins mcnet so monitors probe the stack by internal service name.
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:1
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- kuma_data:/app/data
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
volumes:
|
||||
drasl_state:
|
||||
mc_data:
|
||||
kuma_data:
|
||||
|
||||
networks:
|
||||
mcnet:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Publishes the stack's service names on .local via the HOST's avahi-daemon
|
||||
# (over its D-Bus socket — no second daemon, avoids the :5353 collision).
|
||||
# Requires the host to run avahi-daemon and the socket to be mounted (see
|
||||
# docker-compose.avahi.yml). Used when ENABLE_MDNS=true and BASE_DOMAIN=*.local.
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache avahi-tools
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Publish A records for the stack's service names on .local via the HOST's
|
||||
# avahi-daemon (reached over its mounted D-Bus socket). One avahi-publish per
|
||||
# name; if any drops the container exits so Docker restarts it.
|
||||
set -eu
|
||||
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset}"
|
||||
: "${HOST_LAN_IP:?HOST_LAN_IP unset}"
|
||||
|
||||
case "$BASE_DOMAIN" in
|
||||
*.local) ;;
|
||||
*) echo "WARN: mDNS resolves only .local — BASE_DOMAIN=$BASE_DOMAIN will NOT be discoverable via avahi. Set BASE_DOMAIN=*.local to use mDNS." >&2 ;;
|
||||
esac
|
||||
|
||||
bus="${DBUS_SYSTEM_BUS_ADDRESS:-unix:path=/run/dbus/system_bus_socket}"
|
||||
sock=${bus#unix:path=}
|
||||
if [ ! -S "$sock" ]; then
|
||||
echo "ERROR: host D-Bus socket '$sock' not found. Mount the host's" >&2
|
||||
echo " /run/dbus/system_bus_socket and ensure avahi-daemon runs on the host." >&2
|
||||
exit 1
|
||||
fi
|
||||
export DBUS_SYSTEM_BUS_ADDRESS="$bus"
|
||||
|
||||
pids=""
|
||||
for n in "$BASE_DOMAIN" "auth.$BASE_DOMAIN" "packwiz.$BASE_DOMAIN" "mc.$BASE_DOMAIN"; do
|
||||
echo "mDNS publish (host avahi): $n -> $HOST_LAN_IP"
|
||||
avahi-publish -a "$n" "$HOST_LAN_IP" &
|
||||
pids="$pids $!"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
wait -n $pids 2>/dev/null || wait
|
||||
@@ -0,0 +1,33 @@
|
||||
# NMSR-as-a-Service (https://github.com/NickAcPT/nmsr-rs) — built from source.
|
||||
# Upstream ships no published image, so we vendor a pinned build. Adapted from
|
||||
# the upstream Dockerfile but: pinned to a commit (reproducible), and the config
|
||||
# is bind-mounted at runtime (not COPYed) so it can change without a rebuild.
|
||||
#
|
||||
# Pin — bump deliberately:
|
||||
ARG NMSR_REF=948ba4bc027b
|
||||
|
||||
FROM rust:slim-bookworm AS builder
|
||||
ARG NMSR_REF
|
||||
WORKDIR /tmp
|
||||
RUN apt-get update -y \
|
||||
&& apt-get --no-install-recommends install -y git libssl-dev pkg-config ca-certificates
|
||||
RUN git clone https://github.com/NickAcPT/nmsr-rs/
|
||||
WORKDIR /tmp/nmsr-rs
|
||||
RUN git checkout "${NMSR_REF}"
|
||||
# target-cpu=native: this image is built on the host that runs it (self-host).
|
||||
RUN RUSTFLAGS="-Ctarget-cpu=native" \
|
||||
cargo build --release --bin nmsr-aas --features ears --package nmsr-aas
|
||||
|
||||
FROM rust:slim-bookworm
|
||||
# mesa-vulkan-drivers provides lavapipe (software Vulkan) for headless rendering.
|
||||
RUN apt-get update -y \
|
||||
&& apt-get --no-install-recommends install -y mesa-vulkan-drivers ca-certificates
|
||||
WORKDIR /nmsr
|
||||
COPY --from=builder /tmp/nmsr-rs/target/release/nmsr-aas /nmsr/nmsr-aas
|
||||
ENV NMSR_USE_SMAA=1 \
|
||||
NMSR_SAMPLE_COUNT=1 \
|
||||
WGPU_BACKEND=vulkan \
|
||||
RUST_BACKTRACE=1
|
||||
EXPOSE 8080
|
||||
# config.toml is bind-mounted by docker-compose.nmsr.yml.
|
||||
CMD ["/nmsr/nmsr-aas", "-c", "/nmsr/config.toml"]
|
||||
@@ -3,7 +3,10 @@
|
||||
# -> drasl/config/config.toml (gitignored). Only ${BASE_DOMAIN} is substituted.
|
||||
# Reached only via Caddy at auth.${BASE_DOMAIN}; drasl has no published host port.
|
||||
|
||||
BaseURL = "http://auth.${BASE_DOMAIN}"
|
||||
# Public scheme is HTTPS — the host nginx terminates TLS in front of caddy.
|
||||
# Drasl builds absolute URLs (textures, authlib-injector API location) from this;
|
||||
# an http:// value triggers mixed-content + broken login on the https origin.
|
||||
BaseURL = "https://auth.${BASE_DOMAIN}"
|
||||
Domain = "auth.${BASE_DOMAIN}"
|
||||
ListenAddress = "0.0.0.0:25585" # internal; Caddy reverse-proxies to it
|
||||
DefaultAdminUsernames = ["admin"]
|
||||
|
||||
@@ -24,19 +24,15 @@ export const site = {
|
||||
dust: true,
|
||||
},
|
||||
|
||||
// Connect target shown to guests. No port needed — a dnsmasq SRV record
|
||||
// (_minecraft._tcp.mc.<domain>) points clients at 25565.
|
||||
serverAddress: `mc.${BASE_DOMAIN}`,
|
||||
// Connect target shown to guests. No port needed (defaults to 25565).
|
||||
serverAddress: `${BASE_DOMAIN}`,
|
||||
|
||||
// Drasl auth web UI + authlib-injector API root.
|
||||
authUrl: `http://auth.${BASE_DOMAIN}`,
|
||||
authlibUrl: `http://auth.${BASE_DOMAIN}/authlib-injector`,
|
||||
// Drasl auth web UI + authlib-injector API root (public HTTPS).
|
||||
authUrl: `https://auth.${BASE_DOMAIN}`,
|
||||
authlibUrl: `https://auth.${BASE_DOMAIN}/authlib-injector`,
|
||||
|
||||
// packwiz modpack entrypoint.
|
||||
packwizUrl: `http://packwiz.${BASE_DOMAIN}/pack.toml`,
|
||||
|
||||
// Caddy local CA root — guests import this to trust the offline asset mirror.
|
||||
caCertUrl: `http://${BASE_DOMAIN}/ca.crt`,
|
||||
// packwiz modpack entrypoint (public HTTPS, pack. subdomain).
|
||||
packwizUrl: `https://pack.${BASE_DOMAIN}/pack.toml`,
|
||||
|
||||
// Static server-list panel (Status section). No fake live count — see plan
|
||||
// 09-landing.md option B.
|
||||
|
||||
@@ -54,9 +54,6 @@ export interface UI {
|
||||
pb: string; // after it
|
||||
copy: string;
|
||||
registerPre: string;
|
||||
caPre: string;
|
||||
caLink: string;
|
||||
caPost: string;
|
||||
};
|
||||
s3: { kicker: string; h: string; p: string };
|
||||
s4: {
|
||||
@@ -137,9 +134,6 @@ export const ui: Record<Lang, UI> = {
|
||||
pb: "account using this URL, then log in with your Ulicraft credentials.",
|
||||
copy: "Copy",
|
||||
registerPre: "Need credentials? Register at",
|
||||
caPre: "Offline party? Download and trust the",
|
||||
caLink: "local CA certificate",
|
||||
caPost: "so the game-file mirror works over HTTPS.",
|
||||
},
|
||||
s3: {
|
||||
kicker: "Modpack",
|
||||
@@ -226,9 +220,6 @@ export const ui: Record<Lang, UI> = {
|
||||
pb: "con esta URL y luego inicia sesión con tus credenciales de Ulicraft.",
|
||||
copy: "Copiar",
|
||||
registerPre: "¿Sin credenciales? Regístrate en",
|
||||
caPre: "¿Fiesta sin internet? Descarga y confía en el",
|
||||
caLink: "certificado CA local",
|
||||
caPost: "para que el mirror de archivos funcione por HTTPS.",
|
||||
},
|
||||
s3: {
|
||||
kicker: "Modpack",
|
||||
@@ -315,9 +306,6 @@ export const ui: Record<Lang, UI> = {
|
||||
pb: "kontu bat URL honekin, eta gero hasi saioa zure Ulicraft kredentzialekin.",
|
||||
copy: "Kopiatu",
|
||||
registerPre: "Kredentzialik ez? Erregistratu hemen:",
|
||||
caPre: "Internetik gabeko festa? Deskargatu eta fidatu",
|
||||
caLink: "tokiko CA ziurtagiriaz",
|
||||
caPost: "fitxategi-mirrorrak HTTPS bidez ibil dadin.",
|
||||
},
|
||||
s3: {
|
||||
kicker: "Modpacka",
|
||||
|
||||
@@ -207,9 +207,6 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
<p>
|
||||
{t.join.s2.registerPre} <a href={site.authUrl}>{site.authUrl}</a>.
|
||||
</p>
|
||||
<span class="hint">
|
||||
{t.join.s2.caPre} <a href={site.caCertUrl}>{t.join.s2.caLink}</a> {t.join.s2.caPost}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Ulicraft host nginx — TLS terminator in FRONT of caddy.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Alternative to ulicraft.conf.tmpl. Instead of nginx serving the static
|
||||
# files + proxying only drasl, here caddy (the docker container) is the single
|
||||
# ingress for ALL vhosts and nginx just terminates TLS and reverse-proxies
|
||||
# everything to caddy by Host header.
|
||||
# caddy (in docker-compose.yml) is the single internal ingress for ALL vhosts;
|
||||
# this host nginx terminates TLS and reverse-proxies everything to caddy by Host
|
||||
# header. Prefer tooling/render-nginx.sh over hand-rendering.
|
||||
#
|
||||
# Caddy must be published on a localhost port — set in .env:
|
||||
# CADDY_HTTP_PORT=8880
|
||||
# CADDY_HTTP_BIND=127.0.0.1
|
||||
# and the stack brought up with the caddy override (NOT the nginx override):
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
||||
# -f docker-compose.static.yml up -d
|
||||
# and the stack brought up:
|
||||
# docker compose up -d
|
||||
#
|
||||
# Render with BOTH vars then install:
|
||||
# Render with the vars then install:
|
||||
# CADDY_HTTP_PORT=8880 BASE_DOMAIN=ulicraft.net APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 \
|
||||
# envsubst '$CADDY_HTTP_PORT $BASE_DOMAIN $APP_DIR' \
|
||||
# < nginx/ulicraft-caddy.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
|
||||
@@ -32,7 +30,7 @@ upstream ulicraft_caddy {
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${BASE_DOMAIN} auth.${BASE_DOMAIN} pack.${BASE_DOMAIN} distribution.${BASE_DOMAIN};
|
||||
server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} pack.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
@@ -40,13 +38,13 @@ server {
|
||||
# caddy routes by the forwarded Host header (apex -> www, auth -> drasl,
|
||||
# pack -> packwiz files).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name ${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
client_max_body_size 4m; # skin uploads (auth) reuse the same proxy block
|
||||
|
||||
@@ -59,14 +57,27 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# www -> apex canonical redirect (needs its own cert; covered by LE_SUBDOMAINS).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name www.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/www.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/www.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
return 301 https://${BASE_DOMAIN}$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name auth.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/auth.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
client_max_body_size 4m; # skin uploads
|
||||
|
||||
@@ -80,13 +91,13 @@ server {
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name pack.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/pack.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/pack.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
@@ -98,13 +109,13 @@ server {
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name distribution.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/distribution.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
@@ -114,3 +125,44 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name avatar.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/avatar.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/avatar.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name status.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/status.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/status.${BASE_DOMAIN}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ulicraft_caddy;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Uptime Kuma uses websockets (socket.io) for live status updates.
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Ulicraft host nginx — TLS ingress with Let's Encrypt certs.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Template. Render with BOTH vars then install:
|
||||
#
|
||||
# APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 BASE_DOMAIN=ulicraft.net \
|
||||
# envsubst '$APP_DIR $BASE_DOMAIN' \
|
||||
# < nginx/ulicraft.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf
|
||||
# sudo ln -sf /etc/nginx/sites-available/ulicraft.conf /etc/nginx/sites-enabled/
|
||||
# sudo nginx -t && sudo systemctl reload nginx
|
||||
#
|
||||
# Prereqs:
|
||||
# - certs issued: tooling/issue-letsencrypt.sh -> $APP_DIR/certs/<name>/{cert,key}.pem
|
||||
# - the docker stack up with the nginx override (publishes drasl on 127.0.0.1:25585):
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
# - nginx's user (www-data) can READ $APP_DIR/{www,pack,custom,certs}
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# HTTP -> HTTPS for every name.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${BASE_DOMAIN} auth.${BASE_DOMAIN} pack.${BASE_DOMAIN};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# Apex — static landing page (./www) + launcher downloads (./mirror/launcher).
|
||||
# /ca.crt from the caddy path is intentionally omitted: LE certs are publicly
|
||||
# trusted, so guests need no CA import.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name ${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/${BASE_DOMAIN}/key.pem;
|
||||
|
||||
# Launcher downloads (populated by tooling/fetch-launcher.sh -> ./mirror/launcher).
|
||||
location /launcher/ {
|
||||
alias ${APP_DIR}/mirror/launcher/;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location / {
|
||||
root ${APP_DIR}/www;
|
||||
index index.html;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
|
||||
# Pack — packwiz metadata (./pack) + custom jars (./custom at /custom/).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name pack.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/pack.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/pack.${BASE_DOMAIN}/key.pem;
|
||||
|
||||
# Locally-hosted custom mod jars live outside ./pack; expose them at /custom/.
|
||||
location /custom/ {
|
||||
alias ${APP_DIR}/custom/;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location / {
|
||||
root ${APP_DIR}/pack;
|
||||
autoindex on;
|
||||
}
|
||||
}
|
||||
|
||||
# Auth — reverse proxy to drasl (published on localhost by the nginx override).
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name auth.${BASE_DOMAIN};
|
||||
|
||||
ssl_certificate ${APP_DIR}/certs/auth.${BASE_DOMAIN}/cert.pem;
|
||||
ssl_certificate_key ${APP_DIR}/certs/auth.${BASE_DOMAIN}/key.pem;
|
||||
|
||||
client_max_body_size 4m; # skin uploads
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:25585;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
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