# 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 `${BASE_DOMAIN}` into the templates (plus the derived `REGISTRATION_*` and `FILES_*` values — see `20-files.md`). DNS is **not** this repo's concern: point `${BASE_DOMAIN}` and all subdomains (`auth.` `avatar.` `status.` `files.` `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) avatar.ulicraft.net → nginx → caddy → nmsr:8080 (skin/avatar renderer) status.ulicraft.net → nginx → caddy → uptime-kuma:3001 (status page) files.ulicraft.net → nginx → caddy → filestash:8334 (player file share, shared login) 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}" ] ``` Result: `http://auth.ulicraft.net/authlib-injector` resolves identically inside the stack (minecraft → caddy) as the public name does 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`; 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 name to the host in the service, not to caddy: ```yaml minecraft: extra_hosts: - "auth.${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.` 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 session validation 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. sync server mods → tooling/sync-server-mods.sh (04-mods.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, mc-status, uptime-kuma and filestash. ## 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. - **Distribution-fed mods** — the modset's source of truth is the HeliosLauncher distribution repo; clients install the full set, the server loads a filtered `both`/`server` subset from `./server-mods`. See `04-mods.md`. - **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//{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 ├── filestash/ │ ├── config.json.tmpl # render-config.sh source (files. share) │ └── config.json # generated, mounted :ro (gitignored — holds hashes) ├── docker/nmsr/ # nmsr image build context ├── server-mods/ # filtered server modset (synced, gitignored) ├── 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-mods.md` — distribution-fed mod volume + server filtering (supersedes `04-packwiz.md`) - `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) - `19-routes.md` — HTTP route reference (nmsr / landing / auth / files + internal) - `20-files.md` — Filestash player file share (`files.`, one shared login) ## Boot/dependency order `caddy` (aliases) → `drasl` → `minecraft` (depends on drasl) → `mc-backup`. `nmsr`, `mc-status`, `uptime-kuma` and `filestash` are always-on and join `mcnet` (no ordering constraints — none of them are on the auth/boot path).