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:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 stash–pull–pop 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. **Stash–pull–pop** — `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
|
||||
|
||||
Reference in New Issue
Block a user