Execute plan/18 (WS1-6) + plan/17.
- WS1: homepage join = 3 steps off packwiz (register -> UlicraftLauncher
-> join); per-OS downloads from a synced launcher-manifest.json (schema +
example committed); Fjord moved to its own /fjord page. Drop packwizUrl.
- WS2/3: mc-status gains an isolated /players endpoint (admin login ->
Drasl GET /players, own client+TTL+token cache, never blocks the status
path); online + registered rosters render shared name+avatar tiles;
AVATAR_MODE env (default headiso).
- WS4: /account skin CRUD via browser-direct Drasl (login -> upload ->
reset), in-memory token, players[0]; register relinks "Manage it here".
- WS5: footer link to status.${BASE_DOMAIN} in every footer.
- WS6: tooling/build-modlist.py generates mods.json + extracted logos from
the distribution forgemods; ModList.astro renders a flat list; stat tile
fed from the count.
Manifest/mods loaders read at build with a process.cwd() fallback (the
import.meta.url-only path does not resolve in Astro's bundled build).
New env: AVATAR_MODE, DRASL_ADMIN_USERNAME, DRASL_ADMIN_PASSWORD (mc-status
only). Generated mods.json / launcher-manifest.json / logos are gitignored;
.example.json files document the shapes. Build: 12 pages; mc-status builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
6.9 KiB
Markdown
154 lines
6.9 KiB
Markdown
# Ulicraft Server — Plan Overview
|
|
|
|
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 one env var in `.env`:
|
|
|
|
```
|
|
BASE_DOMAIN=ulicraft.net # all services are subdomains of this
|
|
```
|
|
|
|
`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.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)
|
|
```
|
|
|
|
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. caddy gets network aliases so the stack
|
|
resolves its own subdomains internally:
|
|
|
|
```yaml
|
|
caddy:
|
|
networks:
|
|
mcnet:
|
|
aliases: [ "auth.${BASE_DOMAIN}", "pack.${BASE_DOMAIN}" ]
|
|
```
|
|
|
|
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).
|
|
|
|
#### HTTPS variant (`extra_hosts: host-gateway`)
|
|
|
|
The caddy alias only serves **http** on `mcnet` (port 80). Once the stack moved
|
|
to https URLs (drasl `BaseURL`, `PACKWIZ_URL`, the authlib-injector agent must
|
|
match the client's https endpoint), the internal http alias is no longer enough —
|
|
the container needs to reach a **TLS** terminator holding a valid cert.
|
|
|
|
Caddy does not terminate TLS internally; the host's nginx does (on `0.0.0.0:443`).
|
|
So pin the public names to the host in the service, not to caddy:
|
|
|
|
```yaml
|
|
minecraft:
|
|
extra_hosts:
|
|
- "auth.${BASE_DOMAIN}:host-gateway"
|
|
- "pack.${BASE_DOMAIN}:host-gateway"
|
|
```
|
|
|
|
`/etc/hosts` (extra_hosts) wins over the Docker resolver, so the container reaches
|
|
the host's nginx (valid LE cert) → caddy → service. This is required because
|
|
**containers do not inherit the host's `/etc/hosts`** — on cochi the host resolves
|
|
`auth./pack.` to the public IP, but inside a container the LAN resolver returns a
|
|
dead address (`192.168.0.3:443`, connection refused), which crash-loops the
|
|
packwiz install on boot. `host-gateway` sidesteps DNS entirely.
|
|
|
|
## Build / run flow
|
|
|
|
```
|
|
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)
|
|
|
|
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.
|
|
- **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 # 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 # 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
|
|
├── 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/)
|
|
├── www/ # landing build output — caddy apex (gitignored)
|
|
├── backups/
|
|
└── plan/ # these files
|
|
```
|
|
|
|
## Service plan files
|
|
|
|
- `02-caddy.md` — internal router (vhost conf.d snippets) behind host nginx
|
|
- `03-drasl.md` — auth (password login)
|
|
- `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/fetch/issue scripts
|
|
- `09-landing.md` — Astro+TS guest onboarding page → www/
|
|
- `12-build-order.md` — commit-per-task build sequence
|
|
- `14-deploy.md` — redeploy to the production host
|
|
- `15-letsencrypt.md` — Let's Encrypt (OVH DNS-01) + host nginx ingress
|
|
- `17-mod-shortlist.md` — kitchen-sink gap doc + landing mod-list dataset (PLANNED)
|
|
- `18-landing-rework.md` — join flow / rosters / account / mod list / footer (PLANNED)
|
|
|
|
## Boot/dependency order
|
|
|
|
`caddy` (aliases) → `drasl` → `minecraft` (depends on drasl + pack served by
|
|
caddy) → `mc-backup`. `nmsr` and `uptime-kuma` are always-on and join `mcnet`.
|