refactor!: unify into one compose, drop airgap/mirror/dnsmasq/avahi/blessingskin
Collapse the override-file matrix into a single docker-compose.yml holding the whole stack: drasl, minecraft, mc-backup, caddy, nmsr, uptime-kuma. Bring-up is now just `docker compose up -d --build`. Removed features (dead-ends for this deployment): - airgap / full asset mirror (mirror-airgap.sh, mirror-mods.sh, 20-mirror.caddy, caddy local-CA export, /ca.crt) - dnsmasq + avahi/mDNS + dns-records.sh + mdns-host-setup.sh + ENABLE_MDNS; DNS is now handled outside this repo (HOST_LAN_IP dropped) - Blessing Skin auth variant (compose + 00-core-blessingskin.caddy + BS_* env) - host-nginx-static variant (docker-compose.nginx.yml, nginx/ulicraft.conf.tmpl) - build-stack.sh and its run modes (online/airgap/core) Production ingress is the only path now: host nginx terminates TLS (LE certs) in front of caddy on a localhost-only port; caddy routes every vhost by Host header. Launcher downloads move mirror/launcher -> launcher/. Docs: README, deploy skill, and plan/ rewritten to match; obsolete plan docs (dnsmasq, blessing-skin, assets-mirror, full-airgap-mirror, dns-and-run-modes) deleted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,124 +1,127 @@
|
||||
# Ulicraft Server — Plan Overview
|
||||
|
||||
Self-hosted modded Minecraft LAN-party stack, designed to run **fully offline**
|
||||
(air-gapped) during the party while remaining a persistent homelab service.
|
||||
Self-hosted modded Minecraft stack — a single Docker Compose file holds the
|
||||
entire thing, fronted by the host's own nginx (Let's Encrypt TLS) and a caddy
|
||||
internal router. Built for a LAN party but runs as a persistent homelab service.
|
||||
Assumes the internet is present.
|
||||
|
||||
## Single source of configuration
|
||||
|
||||
Everything is driven by two env vars in `.env`:
|
||||
Everything is driven by one env var in `.env`:
|
||||
|
||||
```
|
||||
BASE_DOMAIN=ulicraft.local # all services are subdomains of this
|
||||
HOST_LAN_IP=192.168.x.x # the Docker host's LAN IP (dnsmasq target)
|
||||
BASE_DOMAIN=ulicraft.net # all services are subdomains of this
|
||||
```
|
||||
|
||||
> `.local` collides with mDNS/Bonjour (RFC 6762). macOS + Linux Avahi may resolve
|
||||
> `*.local` via multicast and bypass dnsmasq. Accepted risk for now; alternatives
|
||||
> if it bites: `.lan`, `home.arpa`. Flagged, user chose `.local`.
|
||||
`render-config.sh` substitutes **only** `${BASE_DOMAIN}` into the templates.
|
||||
DNS is **not** this repo's concern: point `${BASE_DOMAIN}` and all subdomains
|
||||
(`auth.` `pack.` `avatar.` `status.` `distribution.` `www.`) at the host running
|
||||
nginx; that is configured outside this repo.
|
||||
|
||||
## Subdomain / ingress map
|
||||
|
||||
```
|
||||
ulicraft.local → caddy → landing page (launcher downloads + guide)
|
||||
auth.ulicraft.local → caddy → drasl:25585 (auth web UI + Yggdrasil API)
|
||||
packwiz.ulicraft.local → caddy → /srv/pack (pack metadata + mod-jar mirror)
|
||||
assets.ulicraft.local → caddy → /srv/assets (MC asset-object mirror; HTTPS forced — see 10)
|
||||
mc.ulicraft.local:25565 → minecraft (NOT http; raw MC protocol)
|
||||
ulicraft.net → nginx → caddy → landing page (/srv/www) + /launcher/
|
||||
auth.ulicraft.net → nginx → caddy → drasl:25585 (auth web UI + Yggdrasil API)
|
||||
pack.ulicraft.net → nginx → caddy → /srv/pack (packwiz pack metadata)
|
||||
avatar.ulicraft.net → nginx → caddy → nmsr:8080 (skin/avatar renderer)
|
||||
status.ulicraft.net → nginx → caddy → uptime-kuma:3001 (status page)
|
||||
distribution.ulicraft.net → nginx → caddy → /srv/distribution (static, another repo)
|
||||
mc.ulicraft.net:25565 → minecraft (raw MC protocol, not HTTP)
|
||||
```
|
||||
|
||||
Caddy is the only HTTP ingress (plain `:80` for now, TLS/step-ca later).
|
||||
drasl's host port is removed — internal only, reached through Caddy.
|
||||
The host's own nginx is the only public ingress: it terminates TLS with Let's
|
||||
Encrypt certs and reverse-proxies every vhost to caddy by Host header. caddy is
|
||||
published on a **localhost-only** port (`CADDY_HTTP_BIND=127.0.0.1`,
|
||||
`CADDY_HTTP_PORT=8880`) and acts as the internal router. drasl, nmsr and
|
||||
uptime-kuma have no host ports — they are reached only through caddy.
|
||||
|
||||
### Internal resolution trick
|
||||
|
||||
Containers use the Docker resolver, NOT dnsmasq. Caddy gets network aliases so
|
||||
the stack resolves its own subdomains internally:
|
||||
Containers use the Docker resolver. caddy gets network aliases so the stack
|
||||
resolves its own subdomains internally:
|
||||
|
||||
```yaml
|
||||
caddy:
|
||||
networks:
|
||||
mcnet:
|
||||
aliases: [ "auth.${BASE_DOMAIN}", "packwiz.${BASE_DOMAIN}" ]
|
||||
aliases: [ "auth.${BASE_DOMAIN}", "pack.${BASE_DOMAIN}" ]
|
||||
```
|
||||
|
||||
Result: `http://auth.ulicraft.local/authlib-injector` and
|
||||
`http://packwiz.ulicraft.local/pack.toml` resolve identically inside and outside
|
||||
the stack. dnsmasq exists **only for guest laptops**.
|
||||
Result: `http://auth.ulicraft.net/authlib-injector` and
|
||||
`http://pack.ulicraft.net/pack.toml` resolve identically inside the stack
|
||||
(minecraft → caddy) as the public names do outside it (client → nginx → caddy).
|
||||
|
||||
## Online vs offline split
|
||||
## Build / run flow
|
||||
|
||||
```
|
||||
PRE-PARTY (internet, run once):
|
||||
1. curate packwiz pack → see 04-packwiz.md
|
||||
2. mirror mod jars + rewrite URLs → tooling/mirror-mods.sh (08-tooling.md)
|
||||
3. pre-bake server volume → TYPE=NEOFORGE install (05-minecraft.md)
|
||||
4. download launcher releases → tooling/fetch-launcher.sh (06-launcher.md)
|
||||
5. capture+mirror client air-gap → tooling/mirror-airgap.sh (11-full-airgap-mirror.md)
|
||||
6. build landing page → cd landing && pnpm run build → www/ (09-landing.md)
|
||||
7. render configs from templates → tooling/render-config.sh (08-tooling.md)
|
||||
PREP (run once, online):
|
||||
1. render configs → tooling/render-config.sh (08-tooling.md)
|
||||
2. build landing page → cd landing && pnpm run build → www/ (09-landing.md)
|
||||
3. download launcher releases → tooling/fetch-launcher.sh (06-launcher.md)
|
||||
4. curate packwiz pack → see 04-packwiz.md
|
||||
5. (TLS) issue Let's Encrypt certs → tooling/issue-letsencrypt.sh (15-letsencrypt.md)
|
||||
6. (ingress) render+install nginx → tooling/render-nginx.sh --install (15-letsencrypt.md)
|
||||
|
||||
PARTY (air-gapped):
|
||||
dnsmasq : *.ulicraft.local → HOST_LAN_IP
|
||||
caddy : local CDN + landing page
|
||||
drasl : password login (NO Keycloak/OIDC for now)
|
||||
minecraft: TYPE=CUSTOM, no Mojang re-resolution
|
||||
guests : install Fjord launcher, import pack, add authlib account → auth subdomain
|
||||
UP:
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Bring-up is a single command. There are no run modes and no override compose
|
||||
files — one `docker-compose.yml` holds drasl, minecraft, mc-backup, caddy, nmsr
|
||||
and uptime-kuma.
|
||||
|
||||
## Why each hard call was made
|
||||
|
||||
- **No OIDC/Keycloak (for now)** — drasl password login. Keycloak left out of this
|
||||
stack's scope. Re-add later if desired.
|
||||
- **itzg can't air-gap-boot in NEOFORGE mode** (re-resolves Mojang metadata every
|
||||
start, issue #2340). Pre-bake then switch to `TYPE=CUSTOM`.
|
||||
- **packwiz only serves metadata** — `.pw.toml` points at Modrinth CDN. Offline
|
||||
needs jars mirrored onto Caddy + URL rewrite.
|
||||
- **Fjord launcher** has authlib-injector built in → no manual JVM agent on clients.
|
||||
- **No OIDC/Keycloak (for now)** — drasl password login. Keycloak left out of
|
||||
this stack's scope. Re-add later if desired.
|
||||
- **packwiz only serves metadata** — `.pw.toml` points at Modrinth/CF CDNs;
|
||||
server and clients fetch jars from there. The internet is assumed present.
|
||||
- **Fjord launcher** has authlib-injector built in → no manual JVM agent on
|
||||
clients.
|
||||
- **host nginx in front of caddy** — real Let's Encrypt certs (JVM trusts them
|
||||
with no CA import) and a single public TLS terminator; caddy stays an internal
|
||||
Host-routed multiplexer for the stack's own services.
|
||||
|
||||
## Target folder layout
|
||||
|
||||
```
|
||||
.
|
||||
├── docker-compose.yml
|
||||
├── .env / .env.example # BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD
|
||||
├── caddy/Caddyfile # {$BASE_DOMAIN} vhosts + landing page
|
||||
├── dnsmasq/dnsmasq.conf.tmpl # wildcard template
|
||||
├── docker-compose.yml # the entire stack (no overrides)
|
||||
├── .env / .env.example # BASE_DOMAIN, RCON_PASSWORD, CADDY_*, LE block
|
||||
├── caddy/conf.d/ # per-vhost snippets (00-core, 10-static, …)
|
||||
├── nginx/ulicraft-caddy.conf.tmpl # host nginx → caddy template (TLS terminator)
|
||||
├── certs/<name>/{cert,key}.pem # Let's Encrypt output (gitignored)
|
||||
├── drasl/config/
|
||||
│ ├── config.toml.tmpl # envsubst source
|
||||
│ ├── config.toml.tmpl # render-config.sh source
|
||||
│ └── config.toml # generated (gitignored)
|
||||
├── nmsr/ # avatar renderer config + Dockerfile context
|
||||
├── docker/nmsr/ # nmsr image build context
|
||||
├── pack/ # packwiz source of truth
|
||||
├── mirror/ # vendored mod jars + launcher releases (gitignored)
|
||||
├── runtime/ # authlib-injector.jar (legacy; may be unused w/ Fjord)
|
||||
├── tooling/
|
||||
│ ├── render-config.sh # envsubst templates → real configs, halt if unset
|
||||
│ ├── mirror-mods.sh # vendor mod jars + rewrite .pw.toml URLs
|
||||
│ ├── mirror-assets.sh # mirror MC asset objects for offline clients (10)
|
||||
│ └── fetch-launcher.sh # download all Fjord release assets
|
||||
├── launcher/ # vendored Fjord launcher releases (gitignored)
|
||||
├── runtime/ # authlib-injector.jar
|
||||
├── tooling/ # render/fetch/issue scripts (see 08-tooling.md)
|
||||
├── landing/ # Astro+TS landing source (build → www/)
|
||||
│ ├── astro.config.mjs # outDir ../www
|
||||
│ ├── public/logo.png # stone 3D logo
|
||||
│ └── src/{data/site.ts,pages/index.astro}
|
||||
├── www/ # caddy landing page — GENERATED by landing build (gitignored)
|
||||
├── www/ # landing build output — caddy apex (gitignored)
|
||||
├── backups/
|
||||
└── plan/ # these files
|
||||
```
|
||||
|
||||
## Service plan files
|
||||
|
||||
- `01-dnsmasq.md` — LAN wildcard DNS
|
||||
- `02-caddy.md` — ingress + local CDN + landing page
|
||||
- `02-caddy.md` — internal router (vhost conf.d snippets) behind host nginx
|
||||
- `03-drasl.md` — auth (password login)
|
||||
- `04-packwiz.md` — modpack source + mod mirror
|
||||
- `05-minecraft.md` — itzg NeoForge server + offline launch
|
||||
- `04-packwiz.md` — modpack source
|
||||
- `05-minecraft.md` — itzg NeoForge server
|
||||
- `06-launcher.md` — Fjord launcher fetch + distribution
|
||||
- `07-mc-backup.md` — world backups
|
||||
- `08-tooling.md` — render/mirror/fetch scripts
|
||||
- `08-tooling.md` — render/fetch/issue scripts
|
||||
- `09-landing.md` — Astro+TS guest onboarding page → www/
|
||||
- `10-assets-mirror.md` — lighter alternative: objects-only Fjord Assets Server override
|
||||
- `11-full-airgap-mirror.md` — CHOSEN: DNS+TLS transparent mirror for full client air-gap
|
||||
- `12-build-order.md` — commit-per-task build sequence
|
||||
- `13-dns-and-run-modes.md` — domain (.lan), online/airgap modes, party DNS, mDNS option
|
||||
- `14-deploy.md` — redeploy to the production host
|
||||
- `15-letsencrypt.md` — Let's Encrypt (OVH DNS-01) + host nginx ingress
|
||||
|
||||
## Boot/dependency order
|
||||
|
||||
`dnsmasq` → `caddy` (aliases) → `drasl` → `minecraft` (depends on drasl + packwiz served by caddy) → `mc-backup`.
|
||||
`caddy` (aliases) → `drasl` → `minecraft` (depends on drasl + pack served by
|
||||
caddy) → `mc-backup`. `nmsr` and `uptime-kuma` are always-on and join `mcnet`.
|
||||
|
||||
Reference in New Issue
Block a user