From cd79b0e5405598aa59c4bcd2b7804955c46f5112 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 14 Jul 2026 18:23:18 +0200 Subject: [PATCH] docs(files): document filestash across plan/README/deploy skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/skills/deploy/SKILL.md | 49 +++++++++++++++++++++--- CLAUDE.md | 4 ++ README.md | 45 ++++++++++++++++++++-- plan/00-overview.md | 19 +++++++--- plan/02-caddy.md | 10 ++++- plan/12-build-order.md | 9 +++++ plan/14-deploy.md | 69 +++++++++++++++++++++------------- 7 files changed, 164 insertions(+), 41 deletions(-) diff --git a/.claude/skills/deploy/SKILL.md b/.claude/skills/deploy/SKILL.md index 7276e70..c0a4301 100644 --- a/.claude/skills/deploy/SKILL.md +++ b/.claude/skills/deploy/SKILL.md @@ -14,7 +14,7 @@ Redeploy the running stack on the production host. Full reference: - Path: `/home/ubuntu/mc/ulicraft-server-v1` - Branch: `main` - Stack: single unified `docker-compose.yml` (drasl, minecraft, mc-backup, - caddy, nmsr, uptime-kuma) + caddy, nmsr, mc-status, uptime-kuma, filestash) - Auth: Drasl - Restart: **hard** (down → up) — players are disconnected for a few minutes @@ -31,6 +31,18 @@ invoking this skill is the authorization to proceed, but: already up to date and ask whether to restart anyway (they may want a plain restart). If `git fetch` fails (host unreachable, auth), STOP and report. + **If the incoming commits ADD A SERVICE, check `.env` on the host first.** + A new service with `${FOO:?...}` guards (filestash uses them) fails the + **whole compose file**, not just that service — so a missing var means + `up` refuses to start *anything*, and a `--hard` deploy has already run + `down`. The stack stays down. Check before tearing it down: + ```bash + ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && grep -c "^FILES_" .env' + ``` + Same for a new **subdomain**: it needs a cert (`LE_SUBDOMAINS` + + `issue-letsencrypt.sh`) and an nginx vhost — see step 2b, which the deploy + does NOT do for you. + 2. **Pull, ensure authlib, then `update-all.sh --hard`** (single SSH session, halts on any error): ```bash @@ -58,16 +70,31 @@ invoking this skill is the authorization to proceed, but: landing rebuild is unconditional (the gitignored `www/` is never updated by `git pull`), so no separate landing step is needed. +2b. **Only if the deploy changed `nginx/ulicraft-caddy.conf.tmpl`** (a new + subdomain, a new body-size/rate-limit rule, a changed `CADDY_HTTP_PORT`). + `update-all.sh` does **not** touch the host nginx, so a new vhost simply + won't exist and the subdomain 404s / hits the wrong server block: + ```bash + ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && tooling/render-nginx.sh --install' + ``` + It runs `nginx -t` before reloading, so a bad render fails safe. The cert + must already exist (`certs//cert.pem`) or nginx won't load the block — + issue it first with `tooling/issue-letsencrypt.sh` (reads `LE_SUBDOMAINS`). + 3. **Verify.** ```bash ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && docker compose ps' ``` - Confirm `caddy`, `drasl`, `minecraft`, `mc-backup`, `nmsr`, `uptime-kuma` are - Up. Tail the minecraft log briefly and confirm it reaches `Done` (server - ready): + Confirm `caddy`, `drasl`, `minecraft`, `mc-backup`, `nmsr`, `mc-status`, + `uptime-kuma`, `filestash` are Up. Tail the minecraft log briefly and confirm + it reaches the ready marker (match `Done (Ns)! For help` — a bare `Done` + also matches unrelated mod-loading lines): ```bash - ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && timeout 120 docker compose logs -f minecraft' 2>/dev/null | grep -m1 "Done" + ssh cochi 'cd /home/ubuntu/mc/ulicraft-server-v1 && timeout 180 docker compose logs -f minecraft' 2>/dev/null | grep -m1 'Done ([0-9.]*s)! For help' ``` + If a public vhost changed, verify it end-to-end from your workstation rather + than trusting `compose ps` (a container can be Up while nginx never routes to + it): `curl -s -o /dev/null -w '%{http_code}' https:///`. ## Guardrails @@ -81,6 +108,18 @@ invoking this skill is the authorization to proceed, but: `docker compose up -d --force-recreate caddy`, then verify with `docker compose exec caddy cat /etc/caddy/conf.d/`. The `--hard` path (full `down`/`up`) already recreates, so it's unaffected. +- **Same trap for `filestash/config.json`** — also a single-file bind mount, and + it is *re-rendered on every run* by `render-config.sh`, so a rolling + `update-all.sh` leaves filestash reading the stale inode after any `FILES_*` + change (rotating the shared password, flipping `FILES_AUTH`). Apply with + `docker compose up -d --force-recreate filestash`. `--hard` is unaffected. +- **Never let `filestash/config.json` be missing when compose runs.** It is + gitignored and rendered; if it doesn't exist, Docker creates a *directory* at + that path and filestash breaks in a confusing way. `update-all.sh` renders + before `up`, so the normal path is safe — just don't hand-run `docker compose + up` on the host without rendering first. If it happened: `rm -rf + filestash/config.json && tooling/render-config.sh && docker compose up -d + --force-recreate filestash`. - Volumes (`mc_data`, `drasl_state`, `kuma_data`) persist across `down`; the world and monitors are safe. Do NOT pass `-v`/`--volumes` to `down`. - If a step fails, STOP and surface the exact error + the failing command. Do not diff --git a/CLAUDE.md b/CLAUDE.md index cf91a8d..0c24c45 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,6 +175,10 @@ Full design settled across WS1–WS6; all tasks unchecked, ready to execute. distribution forgemods (`tooling/build-modlist.py`), flat alphabetical, pretty names, no categories/links; feeds the "50+" stat tile. - [ ] Server-side perf mods (C2ME, etc.). +- [x] **Filestash file share** (`files.`, 2026-07-14) — live in prod. One shared + htpasswd login, writable, rendered `:ro` config. See `plan/20-files.md`. + Follow-ups: add the Uptime Kuma HTTP monitor for `files.`; the share is + **outside mc-backup** and unquota'd (accepted — eyeballed). - [ ] **Verify NeoForge version** against current stable before first deploy. - [x] **Bootstrap Drasl admin** account (`admin`, 2026-06-10). Key is `DefaultAdmins` (not `DefaultAdminUsernames`); first admin needs an invite-flip. diff --git a/README.md b/README.md index 152e0a3..efca293 100644 --- a/README.md +++ b/README.md @@ -15,24 +15,27 @@ One compose file, one `docker compose up -d`: | `drasl` | unmojang/drasl | Yggdrasil auth + skins (password login) | | `caddy` | caddy:alpine | internal ingress: routes every vhost by Host header | | `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}` | | `mc-backup` | itzg/mc-backup | world backups every 6h via RCON | Vhosts (all proxied through the host nginx → caddy): -- apex `${BASE_DOMAIN}` — landing page + `/launcher/` downloads +- apex `${BASE_DOMAIN}` — landing page + `/launcher/` downloads + `/api/mcstatus/*` - `auth.` — Drasl - `avatar.` — NMSR - `status.` — Uptime Kuma +- `files.` — Filestash player file share (one shared login, writable) - `distribution.` — static site from another repo (`DISTRIBUTION_WEB_ROOT`) Config lives in `.env`: `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT` / -`CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`. See `.env.example`. +`CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`, the `FILES_*` block. See `.env.example`. ## DNS Handled **outside this repo**. Point `${BASE_DOMAIN}` and every subdomain -(`auth. avatar. status. distribution. www.`) at the host running nginx. +(`auth. avatar. status. files. distribution. www.`) at the host running nginx. ## Prerequisites @@ -121,6 +124,7 @@ 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) | Internal-name targets (`minecraft`, `drasl:25585`, `nmsr:8080`) isolate "service down" from "ingress/TLS/DNS down"; public-URL targets test the whole chain. @@ -153,6 +157,41 @@ curl -H "Host: ulicraft.net" \ Wired in `landing/src/data/site.ts` (`statusApi`) and `caddy/conf.d/10-static.caddy`. +## Player file share (Filestash) + +`files.${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 +browser and *not* a mod mirror: world snapshots contain every player's inventory +and coordinates, and the modset already ships via `distribution.`. + +Full design, deploy steps and gotchas: **`plan/20-files.md`**. The short version: + +- Config is `filestash/config.json`, **rendered from a template and mounted + `:ro`** — git is the source of truth. The admin console at `/admin` loads but + **cannot save**; that's deliberate. Change config by editing the `.tmpl`, + re-rendering, and **recreating** the container (`up -d --force-recreate + filestash` — a single-file bind mount pins the inode, so `restart` reads the + stale file). +- Files live in `${FILES_DATA_DIR}` on the host, outside the repo. **Not backed + up** by mc-backup — treat the share as disposable. +- `FILES_MOUNT_MODE=ro|rw` is the literal mount flag: `ro` makes the share + read-only at the *kernel*, whatever the UI thinks. +- `FILES_AUTH=htpasswd|passthrough` — `passthrough` means **no login at all**. + Only valid once the host is off the public internet: an unauthenticated + *writable* share on a public domain is an open upload relay. + +Three traps that cost real time (all enforced or documented in the tooling): + +- **`FILES_PASSWORD_HASH` must be `$6$`** (`openssl passwd -6`). The htpasswd + plugin's bcrypt branch only matches `$2a$`, so a `$2y$` hash from + `htpasswd -B` fails *every* login, silently. `render-config.sh` rejects it. +- **The `local` storage backend is admin-gated** — without `FILES_LOCAL_SECRET` + reaching it through the connection params, every login dies with + `backend error - Not Allowed`, which looks like bad credentials and isn't. +- **Don't set `APPLICATION_URL`/`ADMIN_PASSWORD` env** — either makes Filestash + rewrite `config.json` at boot, which fails against the `:ro` mount. + ## Join (guests) 1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS. diff --git a/plan/00-overview.md b/plan/00-overview.md index 93708f2..d89149b 100644 --- a/plan/00-overview.md +++ b/plan/00-overview.md @@ -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). diff --git a/plan/02-caddy.md b/plan/02-caddy.md index 9957ecb..7c3567b 100644 --- a/plan/02-caddy.md +++ b/plan/02-caddy.md @@ -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 diff --git a/plan/12-build-order.md b/plan/12-build-order.md index 74eb7ea..4fed56a 100644 --- a/plan/12-build-order.md +++ b/plan/12-build-order.md @@ -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 diff --git a/plan/14-deploy.md b/plan/14-deploy.md index 6a5314e..12a5cf7 100644 --- a/plan/14-deploy.md +++ b/plan/14-deploy.md @@ -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