From e25c69e2a1c326343ee97407b26f8f49a54b4269 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 19 Aug 2026 02:53:43 +0100 Subject: [PATCH] Make multi-instance work without a domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An instance's isolation never needed a domain — its own slug, dir, secrets, IP and randomly-allocated host port already make two copies independent. But the routing layer assumed one, so a LAN-only box got a broken instance rather than a port-served one. Four fixes: - instanceCreate now rewrites the parent-service column of the cloned config's PORT_ rows to match the service names it stamps into the compose. That value is stored as network_resources.parent_service and joined against the compose-derived service names, so an instance left carrying the TYPE's service name matched nothing: it rendered in the WebUI with no port, no URL and no login row despite being up and reachable. - `instance create --local` (plus a LAN-only toggle in the modal) forces every port to access=private, traefik=false, for a second copy that should stay off the domain even when one is configured. - initializeAppVariables forces the traefik column false when no CFG_DOMAIN_n is set. Previously a traefik=true port with an empty domain stamped Host(`app.`) — a trailing-dot host matching nothing — and dragged APP_URL to https://app. with it, breaking every app that builds its links from APP_URL. host_setup is blanked for the same reason. The published host port is untouched; access type, not the traefik flag, gates allocation. - APP_URL's direct host-port branch now prefers a new $local_ip_v4 (the source IP for the default route) over $public_ip_v4, which is the WAN address from an external resolver. LibrePortal never forwards ports, so the WAN address was unreachable for exactly the LAN/VPN clients that branch serves. Co-Authored-By: Claude Opus 5 --- .../components/apps/core/css/apps.css | 35 ++++++++ .../apps/core/js/instance-manager.js | 51 ++++++++--- .../frontend/core/tasks/js/task-actions.js | 6 +- .../frontend/core/tasks/js/task-router.js | 2 +- .../instance/cli_instance_commands.sh | 31 +++++-- .../commands/instance/cli_instance_header.sh | 5 +- .../config/docker/docker_config_setup_data.sh | 2 +- .../tags/processors/tags_processor_app_url.sh | 12 ++- scripts/instance/instance_create.sh | 88 +++++++++++++++++-- .../network/variables/variables_init_app.sh | 23 +++++ .../source/files/arrays/function_manifest.sh | 6 ++ variables.sh | 15 ++++ 12 files changed, 248 insertions(+), 28 deletions(-) diff --git a/containers/libreportal/frontend/components/apps/core/css/apps.css b/containers/libreportal/frontend/components/apps/core/css/apps.css index f8f1dd7..3bb1600 100644 --- a/containers/libreportal/frontend/components/apps/core/css/apps.css +++ b/containers/libreportal/frontend/components/apps/core/css/apps.css @@ -686,6 +686,41 @@ color: var(--text-secondary); min-height: 14px; } +/* LAN-only toggle. Row-oriented, unlike .lp-instance-field, so the label sits + beside the box; the checkbox keeps its intrinsic size rather than inheriting + the full-width input rule above. */ +.lp-instance-check { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: 14px; + font-size: 13px; + color: var(--text-primary); + cursor: pointer; +} +.lp-instance-check input { + width: 15px; + height: 15px; + flex: 0 0 auto; + accent-color: var(--accent); + cursor: pointer; +} +.lp-instance-check input:disabled { + cursor: default; +} +.lp-instance-check:has(input:disabled) { + cursor: default; + opacity: 0.75; +} +/* The "no domain configured" note sits between the toggle and the preview line. + .lp-instance-hint carries no bottom margin (it normally hangs under a field + that supplies its own), so without this it butts straight into "Will be served + at" and the two read as one run-on paragraph. */ +.lp-instance-check + .lp-instance-hint { + display: block; + margin: -6px 0 14px; + line-height: 1.45; +} .lp-instance-row { display: flex; gap: 12px; diff --git a/containers/libreportal/frontend/components/apps/core/js/instance-manager.js b/containers/libreportal/frontend/components/apps/core/js/instance-manager.js index 8fa6b51..e29ba2e 100644 --- a/containers/libreportal/frontend/components/apps/core/js/instance-manager.js +++ b/containers/libreportal/frontend/components/apps/core/js/instance-manager.js @@ -61,9 +61,14 @@ class InstanceManager { const title = this._typeTitle(typeSlug); this.domains = await this._loadDomains(); - const domainOptions = this.domains.length + // With no CFG_DOMAIN_n set, a subdomain has nothing to attach to — the backend + // would stamp Host(`sub.`) and an unreachable APP_URL. So the modal drops to + // LAN-only and locks the choice, matching what initializeAppVariables enforces + // server-side. Presenting a domain picker here would have been a lie. + const hasDomains = this.domains.length > 0; + const domainOptions = hasDomains ? this.domains.map(d => ``).join('') - : ''; + : ''; const overlay = document.createElement('div'); overlay.id = 'lp-instance-modal'; @@ -74,7 +79,7 @@ class InstanceManager {

New ${this._esc(title)} instance

-

A fully isolated second copy — its own data, database, subdomain, backups and update cadence.

+

A fully isolated second copy — its own data, database, ports, backups and update cadence.

-
+ + ${hasDomains ? '' : 'No domain is configured, so instances are LAN-only. Add one in Admin → Config to route them on a subdomain.'} + +