From e9fceeab99d2e0fe6aee9a8b27a38ae2deded797 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 19 Aug 2026 18:35:33 +0100 Subject: [PATCH] Enable multi-instance on vaultwarden and searxng MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both now clear every guard: services are -prefixed, and their host ports became random in the previous commit. Enabling them surfaced a real bug that would have made vaultwarden instances fail to start, found by dry-running the clone path before trusting the flag. Eight apps define an app-specific compose-tags hook named with the app as a SUFFIX — appSetupComposeTags_vaultwarden — and docker_config_setup_data.sh dispatches it as appSetupComposeTags_${app_name}. The tools rewrite only renamed the _ PREFIX form, so a clone kept the base name: it defined a function nobody calls (colliding with the base app's), its ADMIN_TOKEN and SIGNUPS_ALLOWED tags were never filled, and the pre-start guard would have refused to launch the instance. Now renamed, anchored on the () of a definition so only real function names are touched. The same hooks pass tag NAMES as strings ("VAULTWARDEN_ADMIN_TOKEN_1_TAG"), invisible to the lowercase renames, while the cloned compose had already moved to _..._TAG. Those are rewritten too, mirroring compose rule 4. Verified: the tags the cloned hook sets now match the cloned compose exactly. Also affects matrix, nextcloud, speedtest, pihole, gluetun and wireguard, which ship the same hook shape — latent for those, since none are enabled. WebUI: the instance bar on app details rendered nothing at all for apps without instance support, which reads as "this build has no instance feature" and sends people hunting for a setting that isn't missing. It now states the reason where the pills would be, and names the blocking ports when it can — the port rows are in the config the frontend already holds, so it mirrors _instanceCheckPortsInstanceable (skipping disabled and random rows). The other blocker lives in the compose, which the frontend never sees, so that case is left unexplained rather than guessed at. Bookstack's rewritten compose and tool tree remain byte-identical to the running instances. Co-Authored-By: Claude Opus 5 --- .../components/apps/core/css/apps.css | 18 ++++++++++ .../components/apps/core/js/apps-grid.js | 36 ++++++++++++++++++- containers/searxng/searxng.config | 5 +++ containers/vaultwarden/vaultwarden.config | 5 +++ scripts/instance/instance_create.sh | 20 +++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/containers/libreportal/frontend/components/apps/core/css/apps.css b/containers/libreportal/frontend/components/apps/core/css/apps.css index 1b67208..01fea2c 100644 --- a/containers/libreportal/frontend/components/apps/core/css/apps.css +++ b/containers/libreportal/frontend/components/apps/core/css/apps.css @@ -542,6 +542,24 @@ border: 1px solid var(--border-subtle, var(--border-color)); border-radius: 12px; } +/* The "not available" variant of the bar: same frame, quieter, since it states a + fact rather than offering an action. */ +.instance-family-bar-off { + background: transparent; + border-style: dashed; +} +.instance-family-note { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.45; +} +.instance-family-note code { + font-size: 12px; + padding: 1px 5px; + border-radius: 5px; + background: rgba(var(--text-rgb), 0.08); + color: var(--text-primary); +} .instance-family-label { font-size: 11px; font-weight: 700; diff --git a/containers/libreportal/frontend/components/apps/core/js/apps-grid.js b/containers/libreportal/frontend/components/apps/core/js/apps-grid.js index ab09c95..fd78624 100644 --- a/containers/libreportal/frontend/components/apps/core/js/apps-grid.js +++ b/containers/libreportal/frontend/components/apps/core/js/apps-grid.js @@ -331,7 +331,41 @@ Object.assign(AppsManager.prototype, { const type = isInstance ? myCfg[`CFG_${U(cleanAppName)}_INSTANCE_OF`] : cleanAppName; const typeCfg = cfgOf(type); const isCapable = String(typeCfg[`CFG_${U(type)}_MULTI_INSTANCE`]).toLowerCase() === 'true'; - if (!isCapable) return ''; + + const typeAppEarly = apps.find(x => slugOf(x) === type); + const typeTitleEarly = typeAppEarly ? (typeAppEarly.name || type).split(' - ')[0].trim() : type; + + // Not instanceable: say so where the pills would have been, rather than + // rendering nothing. An empty space reads as "this page has no instance + // feature", which sends people looking for a setting that isn't missing. + // + // Where the reason is knowable from config, give it. A pinned host port is + // the common blocker and the only one visible client-side: the port rows are + // right here, so this mirrors _instanceCheckPortsInstanceable exactly — + // skip `disabled` rows (they publish nothing) and `random` ones (already + // per-instance). Anything else is left unexplained rather than guessed at, + // since the remaining blocker lives in the compose, which the frontend + // never sees. + if (!isCapable) { + const pinned = []; + for (let i = 1; i <= 20; i++) { + const row = typeCfg[`CFG_${U(type)}_PORT_${i}`]; + if (!row) continue; + const f = String(row).split('|'); + const ext = (f[2] || '').split(':')[0]; + if (!ext || ext === 'random' || f[3] === 'disabled') continue; + if (!pinned.includes(ext)) pinned.push(ext); + } + const why = pinned.length + ? `${esc(typeTitleEarly)} publishes on fixed host port${pinned.length > 1 ? 's' : ''} ` + + `${esc(pinned.join(', '))}, which a second copy could not bind.` + : `${esc(typeTitleEarly)} isn't enabled for multiple instances.`; + return ` +
+ Instances + Not available — ${why} +
`; + } const instances = apps .map(slugOf) diff --git a/containers/searxng/searxng.config b/containers/searxng/searxng.config index 7328ec8..0390281 100755 --- a/containers/searxng/searxng.config +++ b/containers/searxng/searxng.config @@ -11,6 +11,11 @@ # HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # CFG_SEARXNG_APP_NAME=searxng +# MULTI_INSTANCE = if true, this app can run as multiple isolated instances +# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on +# apps whose compose identity (container_name, Traefik routers, backup labels) +# is instance-safe — see scripts/instance/instance_create.sh. +CFG_SEARXNG_MULTI_INSTANCE=true CFG_SEARXNG_BACKUP=false CFG_SEARXNG_BACKUP_STRATEGY=auto CFG_SEARXNG_UPDATE_TYPE=auto diff --git a/containers/vaultwarden/vaultwarden.config b/containers/vaultwarden/vaultwarden.config index e88c091..77c34d7 100755 --- a/containers/vaultwarden/vaultwarden.config +++ b/containers/vaultwarden/vaultwarden.config @@ -12,6 +12,11 @@ # MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed) # CFG_VAULTWARDEN_APP_NAME=vaultwarden +# MULTI_INSTANCE = if true, this app can run as multiple isolated instances +# (own data/DB/subdomain/backups) via `libreportal instance create`. Only set on +# apps whose compose identity (container_name, Traefik routers, backup labels) +# is instance-safe — see scripts/instance/instance_create.sh. +CFG_VAULTWARDEN_MULTI_INSTANCE=true CFG_VAULTWARDEN_BACKUP=true CFG_VAULTWARDEN_BACKUP_STRATEGY=auto CFG_VAULTWARDEN_UPDATE_TYPE=auto diff --git a/scripts/instance/instance_create.sh b/scripts/instance/instance_create.sh index 499c0fb..f9a860c 100644 --- a/scripts/instance/instance_create.sh +++ b/scripts/instance/instance_create.sh @@ -312,6 +312,8 @@ _instanceRewriteCompose() { _instanceRewriteTools() { local type="$1" slug="$2" dir="$3" src_compose="$4" local d f base + local type_u="${type^^}" slug_u="${slug^^}" + type_u="${type_u//-/_}"; slug_u="${slug_u//-/_}" # Container identities as they appear in hook/tool code. The per-token rename # below is what catches `docker exec -u git gitea-service …`: the docker-verb @@ -361,6 +363,24 @@ _instanceRewriteTools() { # names and prose. sed -i -E "s/(\b(container|container_name)=\")${type}(\")/\1${slug}\3/g" "$f" + # Function names carrying the app as a SUFFIX. The prefix rule above + # only matches _, so appSetupComposeTags_vaultwarden survived + # untouched — and docker_config_setup_data.sh dispatches that hook as + # appSetupComposeTags_${app_name}, i.e. ..._vaultwarden_work for an + # instance. The clone therefore defined a function nobody calls (under + # a name that collides with the base app's), its compose tags were + # never filled, and the pre-start guard refused to launch the + # instance. Anchored on the () of a definition so only real function + # names are touched. Affects 8 apps that ship this hook shape. + sed -i -E "s/\b([A-Za-z_][A-Za-z0-9_]*)_${type}(\(\))/\1_${slug}\2/g" "$f" + + # The uppercase tag namespace, mirroring rule 4 of the compose rewrite. + # These hooks pass tag NAMES as strings ("VAULTWARDEN_ADMIN_TOKEN_1_TAG"), + # which the lowercase renames above cannot see. The cloned compose has + # already moved to _..._TAG, so leaving these behind would update + # a tag that no longer exists in the file. + sed -i -E "s/\b${type_u}_([A-Z0-9_]*_TAG)\b/${slug_u}_\1/g" "$f" + # The app's own deployed directory. Hooks that build it from # "${containers_dir}/..." instead of "$containers_dir$app_name/..." # read and WRITE the base app's files — adguard's auth adapter edits