docs(files): document filestash across plan/README/deploy skill

Fold files./filestash into the docs that carry the service list, the vhost
map and the deploy procedure — README stack table, 00-overview, 02-caddy,
12-build-order, 14-deploy, and the deploy skill.

Two real gaps the filestash deploy exposed, now guardrails in the skill:
- update-all.sh never touches host nginx, so a NEW SUBDOMAIN silently 404s
  after deploy. It needs issue-letsencrypt.sh + render-nginx.sh --install.
- a new service with ${FOO:?} guards fails the WHOLE compose file, so a
  missing .env var means `up` starts nothing — and --hard has already run
  `down`. Check the host's .env BEFORE tearing the stack down.

Also: filestash/config.json is a single-file bind mount re-rendered every
run, so a rolling update-all leaves it on a stale inode (same trap as the
caddy conf) — force-recreate after any FILES_* change.

Correct the cochi divergence section: `git diff HEAD` on the host is now
empty (verified this deploy — the ff-pull succeeded). The documented
docker-compose.yml/package.json divergence was converged; only untracked
dnsmasq/, caddy-root-ca.crt and a stale pack/ remain. The stash-pull-pop
procedure it prescribed is no longer needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 18:23:18 +02:00
parent 25df9f9398
commit cd79b0e540
7 changed files with 164 additions and 41 deletions

View File

@@ -13,9 +13,10 @@ 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.
`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.` `distribution.` `www.`) at the host running
(`auth.` `avatar.` `status.` `files.` `distribution.` `www.`) at the host running
nginx; that is configured outside this repo.
## Subdomain / ingress map
@@ -25,6 +26,7 @@ ulicraft.net → nginx → caddy → landing page (/srv/www) + /la
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)
```
@@ -90,8 +92,8 @@ UP:
```
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.
files — one `docker-compose.yml` holds drasl, minecraft, mc-backup, caddy, nmsr,
mc-status, uptime-kuma and filestash.
## Why each hard call was made
@@ -119,6 +121,9 @@ and uptime-kuma.
│ ├── 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)
@@ -145,9 +150,11 @@ and uptime-kuma.
- `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 + internal)
- `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` and `uptime-kuma` are always-on and join `mcnet`.
`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).

View File

@@ -7,7 +7,8 @@ nginx (which terminates TLS — see `15-letsencrypt.md`). caddy is published onl
on a localhost-only port (`CADDY_HTTP_BIND=127.0.0.1`, `CADDY_HTTP_PORT=8880`);
nginx reverse-proxies every public vhost to it by Host header. Roles:
1. **Reverse proxy**`auth.` → drasl, `avatar.` → nmsr, `status.` → uptime-kuma.
1. **Reverse proxy**`auth.` → drasl, `avatar.` → nmsr, `status.` → uptime-kuma,
`files.` → filestash.
2. **Static** — apex serves the landing page + `/launcher/` downloads;
`distribution.` serves a static site from another repo.
@@ -36,10 +37,17 @@ The Caddyfile imports per-vhost snippets from `caddy/conf.d/`:
`DISTRIBUTION_WEB_ROOT`, another repo).
- `40-avatar.caddy``avatar.``nmsr:8080` (skin/avatar renderer).
- `50-status.caddy``status.``uptime-kuma:3001` (status page).
- `60-files.caddy``files.``filestash:8334` (player file share, `20-files.md`).
All vhosts are plain `http://…`; TLS is nginx's job. Mounts: `./www:/srv/www:ro`,
`./launcher:/srv/launcher:ro`, `${DISTRIBUTION_WEB_ROOT}:/srv/distribution:ro`.
> **Don't rewrite the `Host` header for `files.`** — filestash rejects any
> request whose Host doesn't match its configured `general.host`, with *"only
> traffic from X is allowed"* (a 403 that reads like an auth failure). nginx
> forwards the original Host and caddy passes it through untouched. Keep it that
> way.
## Notes / risks
- drasl behind a proxy: with matching `BaseURL` and nginx terminating TLS it is

View File

@@ -50,6 +50,15 @@ Dependency-ordered. Each numbered item = one git commit. Operational steps
17. `feat(nginx): add ulicraft-caddy.conf.tmpl + render-nginx.sh`
18. `feat(tls): add issue-letsencrypt.sh (OVH DNS-01) + LE env block`
## Phase 11 — file share (DONE, 2026-07-14)
19. `feat(files): add Filestash player file share at files.${BASE_DOMAIN}`
compose service (digest-pinned) + `60-files.caddy` + nginx vhost (2g uploads,
`/admin` rate limit) + `filestash/config.json.tmpl` rendered `:ro` +
`FILES_*` env block. One shared htpasswd login, writable. See `20-files.md`.
- [ops] `files` added to `LE_SUBDOMAINS``issue-letsencrypt.sh`, then
`render-nginx.sh --install` (a new subdomain needs BOTH; `update-all.sh`
does not touch host nginx).
## Phase 10 — operational (no commits)
- [ops] Prep online (once): `tooling/render-config.sh`, build landing
(`cd landing && set -a && . ../.env && set +a && pnpm run build` — sources

View File

@@ -56,8 +56,19 @@ Everyday landing/config tweak (no world downtime):
ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && git pull --ff-only && ./update-all.sh'
```
Steps `update-all.sh` runs, in order: `render-config.sh` (drasl/nmsr from `.env`;
halts on a bad `REGISTRATION_MODE`) → `sync-server-mods.sh` (mirror the
> **Same inode trap for `filestash/config.json`.** It is a single-file bind mount
> *and* it is re-rendered on every `update-all.sh` run, so a **rolling** update
> after any `FILES_*` change (rotating the shared password, flipping
> `FILES_AUTH`) leaves filestash reading the stale inode. Apply with
> `docker compose up -d --force-recreate filestash`. `--hard` is unaffected.
> Also: never run `docker compose up` on the host without rendering first — a
> missing `filestash/config.json` makes Docker create a **directory** at that
> path. See `plan/20-files.md`.
Steps `update-all.sh` runs, in order: `render-config.sh` (drasl/nmsr/filestash
from `.env`; halts on a bad `REGISTRATION_MODE`, a bad `FILES_AUTH`, or a
`FILES_PASSWORD_HASH` in a format the htpasswd plugin can't verify) →
`sync-server-mods.sh` (mirror the
`both`/`server` subset from `mods-sides.json` out of `$DISTRIBUTION_WEB_ROOT`
into `./server-mods`) → `build-modlist.py` (regen landing `mods.json` + logos;
**run under `python>=3.11`** — cochi's default `python3` is 3.10, but
@@ -75,30 +86,25 @@ pull` never updates them. `update-all.sh` regenerates both every run
`render-config.sh` changed its config (Drasl's `RequireInvite`/CORS/RateLimit) —
no world downtime.
## Known host-local divergence on `cochi` (uncommitted)
## Host-local divergence on `cochi` — RESOLVED (verified 2026-07-14)
`cochi` carries deliberate local edits that are **not in the repo**, so
`git pull --ff-only` **aborts** ("local changes would be overwritten") whenever
an incoming commit also touches `docker-compose.yml`. As of 2026-06-09 the host
has, uncommitted:
**There are no longer any tracked modifications on the host.** `git diff HEAD`
is empty; `git pull --ff-only` fast-forwards cleanly (confirmed on the filestash
deploy). The old divergence — `docker-compose.yml` forced `http`→`https`,
`landing/package.json`, `landing/pnpm-workspace.yaml` — has been converged into
the repo. The stashpullpop dance previously documented here is **no longer
needed**.
- `docker-compose.yml` — internal URLs forced `http`→`https`
(`JVM_OPTS` authlib-injector), for the in-container HTTPS path. (The old
`PACKWIZ_URL` divergence is moot — packwiz was removed; see `04-mods.md`.)
- `dnsmasq/` (new), `caddy/caddy-root-ca.crt`, `landing/pnpm-workspace.yaml` —
internal DNS + private CA so containers resolve/trust `https://auth.` etc.
- `landing/package.json` modified. (The former `pack/` divergence is resolved —
`pack/` and `custom/` were deleted with the packwiz removal.)
What remains on the host is **untracked only**, so it never blocks a pull:
**Do not steamroll it.** Before a deploy that edits `docker-compose.yml`, either:
1. **Converge** — commit the host edits to `main` + push, then a clean ff-pull
deploys (preferred — ends the divergence). The https/dnsmasq/CA setup belongs
in the repo.
2. **Stashpullpop** — `git stash` → `git pull --ff-only` → `git stash pop`.
Non-destructive; safe only when the incoming and host edits are in different
regions of the file. Stop on a pop conflict.
- `dnsmasq/`, `caddy/caddy-root-ca.crt` — internal DNS + private CA so containers
resolve/trust `https://auth.` etc. Host-local infra, deliberately not in git.
- `pack/` — **stale leftover** from the packwiz era (packwiz was removed; see
`04-mods.md`). Nothing reads it. Safe to delete.
Never `git reset --hard` / discard on the host without confirming first.
Still true, and worth keeping: never `git reset --hard` or discard on the host
without confirming first, and re-check `git status` there before any deploy that
touches tracked files — a *new* host-local edit would reintroduce the abort.
## What survives a hard restart
@@ -116,8 +122,15 @@ restart (a few minutes); they reconnect once `minecraft` is healthy again.
- SSH access as the `cochi` alias.
- `.env` present and complete: `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT`,
`CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`, `REGISTRATION_MODE` (`invite`|`open`,
defaults `invite`), and the Let's Encrypt block (`render-config.sh` halts on an
unset `BASE_DOMAIN` or an invalid `REGISTRATION_MODE`).
defaults `invite`), the `FILES_*` block (`plan/20-files.md`), and the Let's
Encrypt block. `render-config.sh` halts on an unset `BASE_DOMAIN`, an invalid
`REGISTRATION_MODE`/`FILES_AUTH`, or a `FILES_PASSWORD_HASH` the htpasswd
plugin can't verify.
> **A missing `FILES_*` var takes the WHOLE stack down, not just filestash.**
> The compose service uses `${FILES_DATA_DIR:?}` / `${FILES_LOCAL_SECRET:?}`
> guards, and compose fails the entire file on an unset required var — so
> `up` starts *nothing*, and a `--hard` deploy has already run `down`. Check
> `.env` on the host **before** deploying a commit that adds a guarded service.
- Docker + compose plugin, `envsubst`, `jq` (used by `sync-server-mods.sh`;
falls back to `python3` if absent), `git`.
- Host nginx + Let's Encrypt certs installed once — see `15-letsencrypt.md`.
@@ -127,11 +140,15 @@ restart (a few minutes); they reconnect once `minecraft` is healthy again.
## Verify
```bash
docker compose ps # caddy, drasl, minecraft, mc-backup, nmsr, mc-status, uptime-kuma up
docker compose ps # caddy drasl minecraft mc-backup nmsr mc-status uptime-kuma filestash
# self-hosted live status feed (plan/15-mc-status.md):
curl -fsS "https://$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/api/mcstatus/v2/status/java/$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)" | jq .online
docker compose logs -f minecraft # watch for "Done"
# server ready — match the full marker, not a bare "Done" (that also hits mod-load lines)
docker compose logs -f minecraft | grep -m1 'Done ([0-9.]*s)! For help'
curl -fsS "https://auth.$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/" >/dev/null && echo "auth ok"
# files. — 200 = live; anonymous API must be REFUSED (auth is on)
curl -s -o /dev/null -w 'files: %{http_code}\n' "https://files.$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/"
curl -s -H 'X-Requested-With: XmlHttpRequest' "https://files.$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/api/session" # want "Not authorised"
```
## Rollback