Add a section covering cert issuance, the localhost CADDY_HTTP_PORT bind, the caddy/static/distribution compose bring-up, and rendering/installing nginx/ulicraft-caddy.conf.tmpl. Note DISTRIBUTION_WEB_ROOT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
6.6 KiB
Markdown
156 lines
6.6 KiB
Markdown
# 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.
|
||
|
||
## Stack
|
||
|
||
| 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`) |
|
||
| `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.
|
||
|
||
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.
|
||
|
||
## DNS (pick one)
|
||
|
||
Names must resolve to `HOST_LAN_IP`. Use **your own party DNS server** (recommended):
|
||
|
||
```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`.
|
||
|
||
## Prerequisites
|
||
|
||
Docker + Docker Compose; for prep also `pnpm`, `packwiz`, `jq`, `curl`,
|
||
`envsubst`, `sha1sum`.
|
||
|
||
## Launch
|
||
|
||
```sh
|
||
cp .env.example .env # set BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD
|
||
|
||
tooling/build-stack.sh --prep # ONLINE, once: render, build landing,
|
||
# mirror mods+assets, fetch launcher +
|
||
# authlib, pre-bake the server volume
|
||
|
||
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)
|
||
```
|
||
|
||
`--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`
|
||
|
||
## Public TLS (host nginx in front of caddy)
|
||
|
||
The LAN path above runs caddy on `:80`. To expose the stack publicly with real
|
||
certs, the machine's own nginx terminates TLS and reverse-proxies every vhost to
|
||
caddy by Host header. Caddy stays the single router (apex landing, `auth.`
|
||
→ drasl, `pack.` packwiz, `distribution.` static).
|
||
|
||
1. **Issue certs** (OVH DNS-01, no inbound ports needed):
|
||
```sh
|
||
# .env: BASE_DOMAIN, LE_EMAIL, OVH_* creds, LE_SUBDOMAINS="auth pack distribution"
|
||
tooling/issue-letsencrypt.sh # → certs/<name>/{cert,key}.pem
|
||
```
|
||
|
||
2. **Bind caddy to a localhost-only port** so only nginx reaches it (`.env`):
|
||
```sh
|
||
CADDY_HTTP_PORT=8880
|
||
CADDY_HTTP_BIND=127.0.0.1
|
||
```
|
||
|
||
3. **Bring up the caddy ingress** (+ static, + distribution if used):
|
||
```sh
|
||
docker compose -f docker-compose.yml -f docker-compose.caddy.yml \
|
||
-f docker-compose.static.yml -f docker-compose.distribution.yml up -d
|
||
```
|
||
|
||
4. **Render + install the nginx vhost** from `nginx/ulicraft-caddy.conf.tmpl`.
|
||
Substitute `CADDY_HTTP_PORT` (proxy target), `BASE_DOMAIN`, `APP_DIR` (repo
|
||
path on the host, for the cert files):
|
||
```sh
|
||
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
|
||
sudo ln -sf /etc/nginx/sites-available/ulicraft.conf /etc/nginx/sites-enabled/
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
```
|
||
|
||
Re-run step 4 whenever you add a subdomain (e.g. a new vhost) so its server
|
||
block is rendered. `nginx/ulicraft.conf.tmpl` is the older alternative (nginx
|
||
serves the static files itself + proxies only drasl) — pick one, not both.
|
||
|
||
`distribution.${BASE_DOMAIN}` serves a static site whose web root lives in
|
||
another repo: set `DISTRIBUTION_WEB_ROOT` (absolute host path) in `.env`; it's
|
||
bind-mounted read-only via `docker-compose.distribution.yml`.
|
||
|
||
## 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).
|
||
|
||
## 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)
|
||
drasl/config/config.toml.tmpl # auth config (rendered)
|
||
dnsmasq/dnsmasq.conf.tmpl # optional DNS config (rendered)
|
||
docker/avahi/ # mDNS responder 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)
|
||
```
|
||
|
||
See `plan/00-overview.md` for the full design.
|