feat(files): move filestash to upload., serve files. as read-only static browse

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>
This commit is contained in:
oierandClaude Opus 4.8 committed 2026-07-24 17:03:33 +02:00
1 parent 243787c530
commit 16523e4f62
13 files changed
+133 -63

No files matched your search

+12 -8
View File
@@ -17,7 +17,7 @@ One compose file, one `docker compose up -d`:
| `nmsr` | built from source | skin/avatar renderer at `avatar.${BASE_DOMAIN}` |
| `mc-status` | built from source | live server-status JSON for the landing card |
| `uptime-kuma` | louislam/uptime-kuma | status page at `status.${BASE_DOMAIN}` |
| `filestash` | machines/filestash (digest-pinned) | player file share at `files.${BASE_DOMAIN}` |
| `filestash` | machines/filestash (digest-pinned) | player file share (upload) at `upload.${BASE_DOMAIN}` |
| `mc-backup` | itzg/mc-backup | world backups every 6h via RCON |
Vhosts (all proxied through the host nginx → caddy):
@@ -124,7 +124,8 @@ persists in the `kuma_data` volume):
| Landing (apex) | HTTP(s) | `https://${BASE_DOMAIN}` |
| Distribution | HTTP(s) | `https://distribution.${BASE_DOMAIN}` |
| Avatar (NMSR) | HTTP(s) | `https://avatar.${BASE_DOMAIN}` |
| Files (Filestash) | HTTP(s) | `https://files.${BASE_DOMAIN}` (expect 200 — the login page is a 200) |
| Upload (Filestash) | HTTP(s) | `https://upload.${BASE_DOMAIN}` (expect 200 — the login page is a 200) |
| Files (static browse) | HTTP(s) | `https://files.${BASE_DOMAIN}` (caddy dir listing of the share, no auth) |
Internal-name targets (`minecraft`, `drasl:25585`, `nmsr:8080`) isolate "service
down" from "ingress/TLS/DNS down"; public-URL targets test the whole chain.
@@ -159,9 +160,12 @@ Wired in `landing/src/data/site.ts` (`statusApi`) and `caddy/conf.d/10-static.ca
## Player file share (Filestash)
`files.${BASE_DOMAIN}` is a web file share for player media — screenshots,
`upload.${BASE_DOMAIN}` is a web file share for player media — screenshots,
schematics, maps, guides. **One shared username/password for everybody**
(`FILES_USER` / `FILES_PASSWORD_HASH`), read **and** write. It is *not* a backup
(`FILES_USER` / `FILES_PASSWORD_HASH`), read **and** write. The same share is
also exposed **read-only, no-auth** as a plain caddy directory listing at
`files.${BASE_DOMAIN}` — every uploaded file is publicly downloadable there.
It is *not* a backup
browser and *not* a mod mirror: world snapshots contain every player's inventory
and coordinates, and the modset already ships via `distribution.`.
@@ -183,9 +187,9 @@ Full design, deploy steps and gotchas: **`plan/20-files.md`**. The short version
Four traps that cost real time (all enforced or documented in the tooling):
- **`general.host` is a bare hostname**, no scheme (`files.${BASE_DOMAIN}`), with
- **`general.host` is a bare hostname**, no scheme (`upload.${BASE_DOMAIN}`), with
`force_ssl: true`. The SPA builds its redirect origin as `scheme + host`, so a
URL there produces `http://https://files...` and every client-side redirect
URL there produces `http://https://upload...` and every client-side redirect
breaks. The server strips the scheme before its *own* Host check, so `curl`
and the uptime monitor stay **green while every browser is broken**.
- **`FILES_PASSWORD_HASH` must be `$6$`** (`openssl passwd -6`). The htpasswd
@@ -199,8 +203,8 @@ Four traps that cost real time (all enforced or documented in the tooling):
> Verifying this service: a 200 on `/` only proves the server is up. Check the
> browser-facing origin — `curl -s -H 'X-Requested-With: XmlHttpRequest`
> `https://files.${BASE_DOMAIN}/api/config | jq -r .result.origin` must print
> `https://files.${BASE_DOMAIN}` — and re-read `docker compose logs filestash`.
> `https://upload.${BASE_DOMAIN}/api/config | jq -r .result.origin` must print
> `https://upload.${BASE_DOMAIN}` — and re-read `docker compose logs filestash`.
## Join (guests)