Per-host toggle for the read-only, no-auth browse of the shared upload dir,
so it can stay on for the LAN-party host (oxps) and be off on the
internet-facing one (cochi). Defaults to `on` — existing hosts unchanged.
Neither layer has conditionals, so each gets the value substituted in:
- caddy: docker-compose mounts 62-files.${FILES_BROWSE:-on}.caddy at the
fixed conf.d target (62-files.caddy -> 62-files.on.caddy, plus a
comments-only 62-files.off.caddy stub — caddy needs a parseable file
there). Same trick as FILES_MOUNT_MODE.
- nginx: render-nginx.sh splices nginx/files-browse.server.tmpl into
${FILES_BROWSE_SERVER} and adds/omits the name in the port-80 redirect.
This is the gate that matters; caddy is bound to 127.0.0.1.
Applying a change needs render-nginx.sh --install plus a caddy
--force-recreate (single-file bind mount pins the inode). With `off`,
drop `files` from LE_SUBDOMAINS on that host. upload. is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Filestash cannot be served under a subpath (its SPA hardcodes /api, /assets,
/login at root), so it moves to its own subdomain upload.${BASE_DOMAIN}. The
apex-level ask was a static, no-auth file browser at files.${BASE_DOMAIN}: caddy
now file_server browse's the same ${FILES_DATA_DIR} share read-only there.
- filestash general.host -> upload.; nginx upload. vhost keeps the 2g body +
upload streaming + /admin throttle; new files. vhost is a plain static proxy.
- caddy: 60-files.caddy split into 60-upload.caddy (filestash) + 62-files.caddy
(file_server browse of /srv/files); caddy mounts FILES_DATA_DIR :ro.
- LE_SUBDOMAINS += upload (both upload. and files. are vhosts needing certs).
Security: files. is unconditionally public read (no auth), independent of
FILES_AUTH. The htpasswd login only gates writes via upload. Documented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add generators.${BASE_DOMAIN}: a pinned fork of misode.github.io served
with its own mcmeta + vanilla-mcdoc mirror, so it works with zero internet
at the LAN party. Fork lives in ulicraft-generators (branch ulicraft),
pinned at upstream 2bfe5522.
The app is 100% client-side: every data fetch comes from the player's
browser, not from cochi. So offline capability is entirely about which
origins the bundle refers to, plus a mirror to serve them from. The vhost
sets Content-Security-Policy with connect-src 'self' as ENFORCEMENT: an
external origin that sneaks back in (or returns via an upstream merge)
then breaks loudly in prod, on an ordinary day, instead of silently at the
venue with no internet to debug it.
sync-generators-data.sh mirrors ~109M: the 1.21.1 summary/data/atlas tags,
assets-tiny (assets minus hundreds of MB of .ogg — hence no sounds
generator), vanilla-mcdoc symbols.json, and summary/versions/data.min.json,
which comes from a BRANCH head rather than a version tag and runs during
Spyglass init. It verifies a sentinel file per source instead of trusting a
clean clone exit, since a renamed upstream path would otherwise sync
"successfully" and only fail in the browser at the party.
The container serves the bundle and the mirror on one origin, so there is
no CORS, no second cert and no second DNS name.
DEPLOY ORDER MATTERS. GENERATORS_REPO must be set in cochi's .env BEFORE
pulling this: compose uses ${GENERATORS_REPO:?...}, and an unset required
variable makes docker compose fail to parse the file — taking down every
service including Minecraft, not just this one. Full runbook and the rest
of the prerequisites: plan/21-generators.md.
Verified offline in real Chromium with every non-origin request aborted:
zero off-origin attempts, zero same-origin failures, zero console errors,
generators rendering schemas and live JSON.
One shared login (htpasswd) for all players, writable. Config is rendered
from a template and mounted read-only, so git stays authoritative — the
admin console loads but cannot save, by design.
Filestash's OIDC/SAML middlewares are enterprise-only, so Keycloak SSO is
out; FILES_AUTH keeps htpasswd/passthrough switchable for later. Auth-off
(passthrough) is only valid once the host is off the public internet —
render-config.sh warns loudly when rendering it.
Three traps, all found by testing against the pinned image:
- the `local` backend is admin-gated: without LOCAL_BACKEND_SECRET in the
connection params every login fails with "backend error - Not Allowed"
- the htpasswd plugin only accepts $2a$ bcrypt, so a $2y$ hash from
`htpasswd -B` fails every login silently — use `openssl passwd -6`.
render-config.sh rejects the wrong format rather than shipping it
- APPLICATION_URL/ADMIN_PASSWORD env make filestash rewrite config.json at
boot, which fails on the :ro mount — both live in the rendered config
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pack service was already gone from compose/caddy and the landing is
off packwiz. Remove the remaining live references: the pack. TLS vhost in
the host nginx template, pack from LE_SUBDOMAINS, and stale docs in README
and CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Add docker-compose.status.yml (louislam/uptime-kuma) layered on the caddy
ingress, joining mcnet so it probes the stack by internal service name and
the public vhosts end-to-end. Host port bound to 127.0.0.1:3001 only; public
access via nginx -> caddy.
- caddy/conf.d/50-status.caddy: status. -> uptime-kuma:3001
- nginx caddy-front template: status. TLS vhost + websocket upgrade headers
for Kuma's live UI; status. added to the HTTP->HTTPS redirect list
- status added to LE_SUBDOMAINS defaults (.env.example, issue-letsencrypt.sh)
- README: Status monitoring section + recommended monitor list (incl. native
Minecraft-protocol ping)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Strict-Transport-Security (max-age 1y, includeSubDomains) to every TLS
server block in both vhost templates. Combined with the existing port-80
301 redirect, browsers refuse plain HTTP to these names after first visit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tooling/render-nginx.sh: pulls BASE_DOMAIN/CADDY_HTTP_PORT from .env,
defaults APP_DIR to the repo checkout, renders a vhost template (default the
caddy-front one) to stdout, or --install writes + symlinks + nginx -t + reloads.
Document it in the README, replacing the manual envsubst one-liner.
Also add the www.${BASE_DOMAIN} canonical 301->apex block and put www in the
LE_SUBDOMAINS default so its cert is issued.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `http2 on;` directive needs nginx 1.25.1+; older builds reject it. Move
http2 onto the listen line (`listen 443 ssl http2;`) which works on old and
new nginx alike. Applied to both vhost templates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Serve a static site at distribution.${BASE_DOMAIN} whose web root lives in
another repo (DISTRIBUTION_WEB_ROOT, bind-mounted read-only into caddy):
- caddy/conf.d/30-distribution.caddy file_server vhost
- docker-compose.distribution.yml mount snippet + external web root
- issue-letsencrypt.sh / .env.example add distribution to LE_SUBDOMAINS
- ulicraft-caddy.conf.tmpl TLS-front block proxying to caddy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the caddy host-published port configurable (CADDY_HTTP_PORT /
CADDY_HTTP_BIND, default 0.0.0.0:80 unchanged) so the machine's own nginx
can terminate TLS and reverse-proxy to caddy on a localhost-only port.
Add nginx/ulicraft-caddy.conf.tmpl: a TLS-terminator vhost set that proxies
apex/auth/pack to caddy by Host header, letting caddy stay the single ingress
for all routing. Alternative to ulicraft.conf.tmpl (nginx-as-static + drasl
proxy). minecraft still reaches caddy internally over http via mcnet aliases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>