librechat-sandbox: manifest-pinned deploy flow, builder-cache + ECONNRESET pitfalls, branding overlay, keyless image path; current deployed tag
This commit is contained in:
1 parent
bac352f50e
commit
6a0d092e53
1 file changed
+65
-34
@@ -7,14 +7,16 @@ The LibreChat evaluation instance at **librechat-test.inference.coop**
|
|||||||
The single source of truth for the deployment is the repo
|
The single source of truth for the deployment is the repo
|
||||||
**[co-op/librechat-custom](https://git.inference.coop/co-op/librechat-custom)**
|
**[co-op/librechat-custom](https://git.inference.coop/co-op/librechat-custom)**
|
||||||
— its README is the full runbook (config deploy, image build/deploy, rollback,
|
— its README is the full runbook (config deploy, image build/deploy, rollback,
|
||||||
env vars, known state). This doc is the operator-facing summary.
|
env vars, branding overlay, known state). This doc is the operator-facing
|
||||||
|
summary.
|
||||||
|
|
||||||
## Architecture: what lives where
|
## Architecture: what lives where
|
||||||
|
|
||||||
- **App config** — `/app/data/librechat.yaml` (persistent volume, mirrored
|
- **App config** — `/app/data/librechat.yaml` (persistent volume, mirrored
|
||||||
byte-identically in the repo). Holds the "Inference Co-op" custom endpoint
|
byte-identically in the repo). Holds the "Inference Co-op" custom endpoint
|
||||||
(gateway + per-member keys), the 8-model chat picker, speech, web search,
|
(gateway), the 8-model chat picker, speech, web search, and the two MCP
|
||||||
the docs MCP server, and a forward-compatible theme block (see below).
|
servers. Volume edits deploy via `cloudron exec` (base64), never via the
|
||||||
|
image — the image never touches `/app/data`.
|
||||||
- **Env file** — `/app/data/env` (persistent volume). **`start.sh` sources it
|
- **Env file** — `/app/data/env` (persistent volume). **`start.sh` sources it
|
||||||
AFTER the process env, so it wins over `cloudron env`** — that is where
|
AFTER the process env, so it wins over `cloudron env`** — that is where
|
||||||
`APP_TITLE="Inference Co-op"`, `ALLOW_EMAIL_LOGIN=false` (password login
|
`APP_TITLE="Inference Co-op"`, `ALLOW_EMAIL_LOGIN=false` (password login
|
||||||
@@ -26,59 +28,83 @@ env vars, known state). This doc is the operator-facing summary.
|
|||||||
`IMAGE_GEN_OAI_MODEL` (agent image tool → gateway), `SEARXNG_INSTANCE_URL`.
|
`IMAGE_GEN_OAI_MODEL` (agent image tool → gateway), `SEARXNG_INSTANCE_URL`.
|
||||||
- **MCP servers** — `/app/data/mcp/server.py` (docs search, `search_docs`,
|
- **MCP servers** — `/app/data/mcp/server.py` (docs search, `search_docs`,
|
||||||
starts at boot) and `/app/data/mcp/image_server.py` (image generation,
|
starts at boot) and `/app/data/mcp/image_server.py` (image generation,
|
||||||
`generate_image`, per-user startup — see below); both mirrored in the repo
|
`generate_image`, `startup: false`); both mirrored in the repo under
|
||||||
under `mcp/`, stdlib-only Python.
|
`mcp/`, stdlib-only Python.
|
||||||
- **Brand assets** (login logo, favicons, PWA icons, page title) — baked into
|
- **Branding overlay** (logo, favicons, PWA icons, `<title>`, `custom.css`,
|
||||||
a pinned overlay image `FROM` the official appstore image, built from the
|
`custom2.js`) — baked into a pinned overlay image `FROM` the official
|
||||||
repo via `cloudron builder build` and deployed with
|
appstore image. Deployed tag (last verified 2026-10-19):
|
||||||
`cloudron update --image ... --force`. Deployed tag:
|
`registry.inference.coop/co-op/librechat-custom:20261019-fc9b11e`.
|
||||||
`registry.inference.coop/co-op/librechat-custom:20261019-5e214e4`.
|
- **Branding CSS/JS** — `custom.css` overrides LibreChat's Tailwind theme
|
||||||
|
variables on `html.light`/`html.dark` (cream surfaces, co-op green accents)
|
||||||
|
and hides the register/sign-up prompt via
|
||||||
|
`p:has(> a[href*="register"])`; `custom2.js` is a MutationObserver that
|
||||||
|
hides the prompt when React renders it (the auth form renders after
|
||||||
|
`load`). The `interface.theme` block in the yaml remains
|
||||||
|
forward-compatible only (inert on v0.8.8).
|
||||||
|
|
||||||
## Image generation (two member-facing paths)
|
## Image generation (two member-facing paths)
|
||||||
|
|
||||||
1. **MCP tool (easiest)**: the `inference-image` MCP server exposes
|
1. **MCP tool (default, no key ever)**: the `inference-image` MCP server
|
||||||
`generate_image` in the composer's **+** palette — no agent setup. Each
|
exposes `generate_image` in the composer's **+** palette — no agent setup,
|
||||||
member's own co-op API key is injected per-user (LibreChat
|
no key prompt. The server uses the member's `IC_API_KEY` if LibreChat
|
||||||
`customUserVars` → `env`, `startup: false` because the key only exists
|
injected one, otherwise sends a **keyless request** (the gateway bills the
|
||||||
after the member saves it in the MCP panel), so images bill to the
|
request; per-member billing is NOT enforced on this path).
|
||||||
member's own balance (~1–2¢ per 1024² image via `regolo/qwen-image`).
|
`customUserVars` were removed from the live yaml on 2026-10-19 — members
|
||||||
|
are never asked for a key.
|
||||||
2. **Agent tool**: LibreChat's built-in "OpenAI Image Tools"
|
2. **Agent tool**: LibreChat's built-in "OpenAI Image Tools"
|
||||||
(`IMAGE_GEN_OAI_BASEURL`/`IMAGE_GEN_OAI_MODEL` → gateway; per-user keys).
|
(`IMAGE_GEN_OAI_BASEURL`/`IMAGE_GEN_OAI_MODEL` → gateway; members paste
|
||||||
|
their own key — v0.8.8 limitation).
|
||||||
|
|
||||||
## Update policy
|
## Update policy
|
||||||
|
|
||||||
- The app is now **image-sourced** (the `--force` update moved it off the
|
- The app is **image-sourced** (the `--force` update moved it off the
|
||||||
appstore source), so Cloudron will not auto-update it. This is deliberate —
|
appstore source), so Cloudron will not auto-update it. This is deliberate —
|
||||||
same lockdown model as LiteLLM.
|
same lockdown model as LiteLLM.
|
||||||
|
- **Image deploys**: build `--no-cache`, write the new tag into
|
||||||
|
`CloudronManifest.json.dockerImage` (the only editable field), commit, then
|
||||||
|
`cloudron update --app <id> --force`. **`--image` alone is IGNORED** when
|
||||||
|
the manifest sits in the deploy directory. **The remote builder's cache can
|
||||||
|
serve stale COPY layers even when file content changed** (six consecutive
|
||||||
|
"Build successful" runs did on 2026-10-19) — so after every image deploy,
|
||||||
|
verify: the Cloudron API's `manifest.dockerImage`, container-file md5 vs
|
||||||
|
repo md5, the served HTML, and the rendered DOM. Full checklist in the
|
||||||
|
repo README.
|
||||||
- **When a new appstore package ships**: pull its manifest copy into the repo
|
- **When a new appstore package ships**: pull its manifest copy into the repo
|
||||||
(`GET /api/v1/apps/<id> → .manifest`), bump the Dockerfile `FROM` to the new
|
(`GET /api/v1/apps/<id>` → `.manifest`), repoint `dockerImage` to the
|
||||||
`manifest.dockerImage`, rebuild, verify in the sandbox, then update. A
|
rebuilt overlay, bump the Dockerfile `FROM` to the new `manifest.dockerImage`
|
||||||
newer LibreChat also activates the already-present `interface.theme` block
|
(pinned), rebuild, verify in the sandbox, then update.
|
||||||
(co-op colors) automatically — the theme engine does not exist in v0.8.8
|
- **Rollback**: repoint `CloudronManifest.json.dockerImage` to the previous
|
||||||
and the block is silently ignored today.
|
tag and rerun `cloudron update --force` (or the original appstore image).
|
||||||
- **Rollback**: `cloudron update --app <id> --image <previous-tag> --force`
|
Cloudron also snapshots the app before every image update.
|
||||||
(or the original appstore image). Cloudron also snapshots the app before
|
|
||||||
every image update.
|
|
||||||
|
|
||||||
## Hard-won pitfalls (do not re-learn these)
|
## Hard-won pitfalls (do not re-learn these)
|
||||||
|
|
||||||
- `cloudron update` deploys **`manifest.dockerImage`** from the manifest in
|
- `cloudron update` deploys **`manifest.dockerImage`** from the manifest in
|
||||||
the deploy directory — the `--image` flag alone is ignored. The repo
|
the deploy directory — the `--image` flag alone is ignored (and the update
|
||||||
|
output still prints the `--image` value, so it *looks* right). The repo
|
||||||
manifest is a byte-identical copy of the OFFICIAL appstore manifest except
|
manifest is a byte-identical copy of the OFFICIAL appstore manifest except
|
||||||
`dockerImage`; editing anything else in it (or hand-writing one that omits
|
`dockerImage`; editing anything else in it (or hand-writing one that omits
|
||||||
the addons) strips `postgresql`/`mongodb`/`oidc`/`sendmail`/`localstorage`
|
the addons) strips `postgresql`/`mongodb`/`oidc`/`sendmail`/`localstorage`
|
||||||
from the app and crash-loops it (`mkdir: Read-only file system`). That is
|
from the app and crash-loops it (`mkdir: Read-only file system`). That is
|
||||||
what broke the sandbox on 2026-10-18.
|
what broke the sandbox on 2026-10-18.
|
||||||
|
- The remote builder's layer cache can pin COPY steps to old file content
|
||||||
|
even across "successful" rebuilds — use `--no-cache` after asset edits and
|
||||||
|
confirm by comparing container-file md5 against the repo file.
|
||||||
- Deploy `librechat.yaml` only via base64 round-trip. A `sed` insert once
|
- Deploy `librechat.yaml` only via base64 round-trip. A `sed` insert once
|
||||||
broke the YAML indentation and LibreChat **silently fell back to defaults**
|
broke the YAML indentation and LibreChat **silently fell back to defaults**
|
||||||
(all custom branding/models/MCP gone) with a single log line.
|
(all custom branding/models/MCP gone) with a single log line.
|
||||||
|
- Reading large files from the container over `cloudron exec` with a plain
|
||||||
|
`cat` resets the connection (`ECONNRESET`) — pipe through
|
||||||
|
`gzip | base64 | fold` and decode locally.
|
||||||
- `cloudron env set` alone does NOT disable the password form —
|
- `cloudron env set` alone does NOT disable the password form —
|
||||||
`ALLOW_EMAIL_LOGIN` must be flipped in `/app/data/env` (precedence above).
|
`ALLOW_EMAIL_LOGIN` must be flipped in `/app/data/env` (precedence above).
|
||||||
- Image generation in LibreChat is an **Agent tool ("OpenAI Image Tools")**,
|
- Image generation in LibreChat is the **MCP tool** (composer **+** palette);
|
||||||
not a chat-picker button; image models in the chat picker were the source
|
the agent "OpenAI Image Tools" path also exists. Image models in the chat
|
||||||
of the old 500 errors and are deliberately excluded.
|
picker were the source of the old 500 errors and are deliberately excluded.
|
||||||
- No `branding:` block exists in LibreChat's config schema; the login logo
|
- No `branding:` block exists in LibreChat's config schema, and the login
|
||||||
and favicons come from the image overlay.
|
page has no `.login-page`/`.primary` classes to target — theming happens by
|
||||||
|
overriding the Tailwind CSS variables on `html.light`/`html.dark`
|
||||||
|
(`--surface-primary`, `--accent-primary`, …) from the overlay `custom.css`.
|
||||||
|
|
||||||
## Verification (after any change)
|
## Verification (after any change)
|
||||||
|
|
||||||
@@ -92,5 +118,10 @@ Expected: `Custom config file loaded`, `[MCP][inference-docs]` initialized
|
|||||||
with tool `search_docs`, `inference-image` registered (its `Tools: undefined`
|
with tool `search_docs`, `inference-image` registered (its `Tools: undefined`
|
||||||
boot line is expected — per-user startup), `[schedules] engine started`,
|
boot line is expected — per-user startup), `[schedules] engine started`,
|
||||||
`appTitle: Inference Co-op`, `emailLoginEnabled: false`. The login page shows
|
`appTitle: Inference Co-op`, `emailLoginEnabled: false`. The login page shows
|
||||||
the co-op logo, the co-op favicon, and a single "Login with Inference
|
the co-op logo, the co-op favicon, a single "Login with Inference
|
||||||
Cooperative" (Cloudron SSO) path with no email/password form.
|
Cooperative" (Cloudron SSO) path with no email/password form, **no
|
||||||
|
"Don't have an account?" line**, and the cream/green co-op color scheme.
|
||||||
|
|
||||||
|
After an IMAGE deploy, additionally run the four-step verification in the
|
||||||
|
repo README (API `manifest.dockerImage`, container md5 vs repo md5, served
|
||||||
|
HTML tags, rendered DOM colors).
|
||||||
Reference in new issue
Block a user