Make multi-instance work without a domain
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 <noreply@anthropic.com>
This commit is contained in:
parent
63b3af4cfc
commit
e25c69e2a1
@ -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;
|
||||
|
||||
@ -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 => `<option value="${d.number}">${this._esc(d.domain)}</option>`).join('')
|
||||
: '<option value="1">Domain 1 (set one in Admin → Config)</option>';
|
||||
: '<option value="1">No domain configured</option>';
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'lp-instance-modal';
|
||||
@ -74,7 +79,7 @@ class InstanceManager {
|
||||
<h3>New ${this._esc(title)} instance</h3>
|
||||
<button type="button" class="lp-instance-x" aria-label="Close">×</button>
|
||||
</div>
|
||||
<p class="lp-instance-sub">A fully isolated second copy — its own data, database, subdomain, backups and update cadence.</p>
|
||||
<p class="lp-instance-sub">A fully isolated second copy — its own data, database, ports, backups and update cadence.</p>
|
||||
|
||||
<label class="lp-instance-field">
|
||||
<span>Instance name</span>
|
||||
@ -82,7 +87,13 @@ class InstanceManager {
|
||||
<small id="lp-instance-slug" class="lp-instance-hint"></small>
|
||||
</label>
|
||||
|
||||
<div class="lp-instance-row">
|
||||
<label class="lp-instance-check">
|
||||
<input type="checkbox" id="lp-instance-local" ${hasDomains ? '' : 'checked disabled'} />
|
||||
<span>LAN only — no domain, served on its own port</span>
|
||||
</label>
|
||||
${hasDomains ? '' : '<small class="lp-instance-hint">No domain is configured, so instances are LAN-only. Add one in Admin → Config to route them on a subdomain.</small>'}
|
||||
|
||||
<div class="lp-instance-row" id="lp-instance-domain-row">
|
||||
<label class="lp-instance-field">
|
||||
<span>Domain</span>
|
||||
<select id="lp-instance-domain" class="form-control">${domainOptions}</select>
|
||||
@ -110,6 +121,8 @@ class InstanceManager {
|
||||
const subEl = overlay.querySelector('#lp-instance-subdomain');
|
||||
const domEl = overlay.querySelector('#lp-instance-domain');
|
||||
const hostEl = overlay.querySelector('#lp-instance-host');
|
||||
const localEl = overlay.querySelector('#lp-instance-local');
|
||||
const domainRow = overlay.querySelector('#lp-instance-domain-row');
|
||||
const createBtn = overlay.querySelector('.lp-instance-create');
|
||||
|
||||
let subEdited = false;
|
||||
@ -120,14 +133,25 @@ class InstanceManager {
|
||||
const slug = id ? `${typeSlug}_${id}` : '';
|
||||
slugEl.textContent = slug ? `Created as ${slug}` : 'Letters and numbers only';
|
||||
if (!subEdited) subEl.value = slug ? slug.replace(/_/g, '-') : '';
|
||||
const dom = (this.domains.find(d => String(d.number) === domEl.value) || {}).domain || '<your-domain>';
|
||||
const sub = (subEl.value || '').trim();
|
||||
hostEl.textContent = sub ? `${sub}.${dom}` : `…`;
|
||||
|
||||
const isLocal = localEl.checked;
|
||||
domainRow.style.display = isLocal ? 'none' : '';
|
||||
if (isLocal) {
|
||||
// The port is allocated at install time, so it genuinely isn't known yet —
|
||||
// say so rather than invent one. Kept short: the preview line is a single
|
||||
// <code> run and a longer string wraps mid-token inside the modal.
|
||||
hostEl.textContent = 'http://<this-server>:<auto-port>';
|
||||
} else {
|
||||
const dom = (this.domains.find(d => String(d.number) === domEl.value) || {}).domain || '<your-domain>';
|
||||
const sub = (subEl.value || '').trim();
|
||||
hostEl.textContent = sub ? `${sub}.${dom}` : '…';
|
||||
}
|
||||
createBtn.disabled = !id;
|
||||
};
|
||||
nameEl.addEventListener('input', refresh);
|
||||
domEl.addEventListener('change', refresh);
|
||||
subEl.addEventListener('input', refresh);
|
||||
localEl.addEventListener('change', refresh);
|
||||
refresh();
|
||||
setTimeout(() => nameEl.focus(), 30);
|
||||
|
||||
@ -137,10 +161,12 @@ class InstanceManager {
|
||||
overlay.querySelector('.lp-instance-cancel').addEventListener('click', () => this.close());
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) this.close(); });
|
||||
|
||||
createBtn.addEventListener('click', () => this._submit(typeSlug, nameEl.value, domEl.value, subEl.value));
|
||||
createBtn.addEventListener('click', () => this._submit(
|
||||
typeSlug, nameEl.value, domEl.value, subEl.value, localEl.checked
|
||||
));
|
||||
}
|
||||
|
||||
async _submit(typeSlug, name, domainIndex, subdomain) {
|
||||
async _submit(typeSlug, name, domainIndex, subdomain, localOnly) {
|
||||
const id = this._idPart(name);
|
||||
if (!id) return;
|
||||
const slug = `${typeSlug}_${id}`;
|
||||
@ -165,8 +191,11 @@ class InstanceManager {
|
||||
await window.tasksManager.router.routeAction('instance_create', {
|
||||
type: typeSlug,
|
||||
name: name,
|
||||
domainIndex: domainIndex,
|
||||
subdomain: subdomain
|
||||
// Domain/subdomain are meaningless for a LAN-only instance and would
|
||||
// otherwise be passed through to a config the --local pass then overwrites.
|
||||
domainIndex: localOnly ? '' : domainIndex,
|
||||
subdomain: localOnly ? '' : subdomain,
|
||||
localOnly: !!localOnly
|
||||
});
|
||||
this.close();
|
||||
notify(`Creating instance ${slug} — track progress in Tasks.`, 'success');
|
||||
|
||||
@ -34,12 +34,16 @@ class TaskActions {
|
||||
* like any install (button state, task icon, progress), while the verbatim
|
||||
* command carries the optional domain#/subdomain through executeTask.
|
||||
*/
|
||||
async instanceCreate(type, name, domainIndex = '', subdomain = '') {
|
||||
async instanceCreate(type, name, domainIndex = '', subdomain = '', localOnly = false) {
|
||||
try {
|
||||
this.commands.validateCommand('instance_create', { type, name });
|
||||
const parts = ['libreportal', 'instance', 'create', type, name];
|
||||
if (domainIndex) parts.push(domainIndex);
|
||||
if (subdomain) parts.push(subdomain);
|
||||
// LAN-only: no Traefik router, served on its published host port. Appended
|
||||
// last, but the CLI pulls the flag out of the positional list, so it never
|
||||
// shifts domain#/subdomain.
|
||||
if (localOnly) parts.push('--local');
|
||||
// The new instance's slug — mirrors the backend's <type>_<id> scheme so
|
||||
// monitorTask/highlight track the right app once it appears.
|
||||
const id = String(name).toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
@ -22,7 +22,7 @@ class TaskRouter {
|
||||
return await this.actions.installApp(params.appName, params.config, params.resetNetwork);
|
||||
|
||||
case 'instance_create':
|
||||
return await this.actions.instanceCreate(params.type, params.name, params.domainIndex, params.subdomain);
|
||||
return await this.actions.instanceCreate(params.type, params.name, params.domainIndex, params.subdomain, params.localOnly);
|
||||
|
||||
case 'instance_remove':
|
||||
return await this.actions.instanceRemove(params.appName);
|
||||
|
||||
@ -9,24 +9,43 @@
|
||||
cliHandleInstanceCommands()
|
||||
{
|
||||
local action="$initial_command2"
|
||||
local type="$initial_command3"
|
||||
local name="$initial_command4"
|
||||
local domain_idx="$initial_command5"
|
||||
local subdomain="$initial_command6"
|
||||
|
||||
# --local/--lan is pulled out of the positional list rather than given a fixed
|
||||
# slot, so it can be written anywhere after the verb — `instance create
|
||||
# bookstack home --local` reads naturally and still leaves domain#/subdomain
|
||||
# in their documented positions for callers that pass them.
|
||||
local local_only="false"
|
||||
local -a _pos=()
|
||||
local _a
|
||||
for _a in "$initial_command3" "$initial_command4" "$initial_command5" \
|
||||
"$initial_command6" "$initial_command7"; do
|
||||
case "$_a" in
|
||||
"--local"|"--lan") local_only="true" ;;
|
||||
*) _pos+=("$_a") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local type="${_pos[0]}"
|
||||
local name="${_pos[1]}"
|
||||
local domain_idx="${_pos[2]}"
|
||||
local subdomain="${_pos[3]}"
|
||||
|
||||
case "$action" in
|
||||
"create")
|
||||
if [[ -z "$type" || -z "$name" ]]; then
|
||||
isNotice "Usage: libreportal instance create <type> <name> [domain_index] [subdomain]"
|
||||
isNotice "Usage: libreportal instance create <type> <name> [domain_index] [subdomain] [--local]"
|
||||
cliShowInstanceHelp
|
||||
return 1
|
||||
fi
|
||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
||||
instanceCreate "$type" "$name" "$domain_idx" "$subdomain"
|
||||
instanceCreate "$type" "$name" "$domain_idx" "$subdomain" "$local_only"
|
||||
else
|
||||
local _cmd="libreportal instance create $type $name"
|
||||
[[ -n "$domain_idx" ]] && _cmd+=" $domain_idx"
|
||||
[[ -n "$subdomain" ]] && _cmd+=" $subdomain"
|
||||
# Re-invocation must carry the flag or the task-side run would
|
||||
# silently build a Traefik-routed instance instead.
|
||||
[[ "$local_only" == "true" ]] && _cmd+=" --local"
|
||||
cliTaskRun "$_cmd" "install" "${type}_$(instanceIdPart "$name")"
|
||||
fi
|
||||
;;
|
||||
|
||||
@ -13,12 +13,15 @@ cliShowInstanceHelp()
|
||||
echo " its own data, DB, subdomain, backups and update cadence. Only apps"
|
||||
echo " with CFG_<TYPE>_MULTI_INSTANCE=true can be instanced."
|
||||
echo ""
|
||||
echo " libreportal instance create [type*] [name*] [domain#] [subdomain]"
|
||||
echo " libreportal instance create [type*] [name*] [domain#] [subdomain] [--local]"
|
||||
echo " - Provision + install a new instance."
|
||||
echo " type = base app slug (e.g. bookstack)"
|
||||
echo " name - instance name (e.g. blog)"
|
||||
echo " domain# - which CFG_DOMAIN_n to route on (default 1)"
|
||||
echo " subdomain- host label (default <type>-<name>)"
|
||||
echo " --local - LAN only: no Traefik router, served"
|
||||
echo " on its own port. Implied when no"
|
||||
echo " CFG_DOMAIN_n is configured."
|
||||
echo " libreportal instance remove [slug*] - Uninstall + remove an instance (e.g. bookstack_blog)"
|
||||
echo " libreportal instance list [type] - List instances (all, or just for one app type)"
|
||||
echo ""
|
||||
|
||||
@ -97,7 +97,7 @@ dockerConfigSetupFileWithData()
|
||||
ipUpdateComposeTags "$app_name" "$full_file_path"
|
||||
portUpdateComposeTags "$app_name" "$full_file_path"
|
||||
tagsProcessorTrustedDomains "$full_file_path"
|
||||
tagsProcessorAppUrl "$full_file_path" "$app_name" "$public" "$host_setup" "$public_ip_v4"
|
||||
tagsProcessorAppUrl "$full_file_path" "$app_name" "$public" "$host_setup" "$public_ip_v4" "$local_ip_v4"
|
||||
|
||||
###############################################
|
||||
# Mail Server Settings
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Stamps APP_URL_TAG — the canonical URL an app advertises to itself. Apps that
|
||||
# build every link, redirect and asset path from it (Bookstack, Nextcloud,
|
||||
# Mastodon) break outright when it doesn't match how the user actually reaches
|
||||
# them, so the two branches below must each name a host that really resolves.
|
||||
tagsProcessorAppUrl()
|
||||
{
|
||||
local full_file_path="$1"
|
||||
@ -7,6 +11,7 @@ tagsProcessorAppUrl()
|
||||
local is_public="$3"
|
||||
local host_setup="$4"
|
||||
local public_ip_v4="$5"
|
||||
local local_ip_v4="$6"
|
||||
|
||||
local traefik_installed=""
|
||||
if [[ -f "$docker_dir/$db_file" ]] && command -v sqlite3 >/dev/null 2>&1; then
|
||||
@ -21,7 +26,12 @@ tagsProcessorAppUrl()
|
||||
if [[ -f "$docker_dir/$db_file" ]] && command -v sqlite3 >/dev/null 2>&1; then
|
||||
external_port=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT resource_value FROM network_resources WHERE app_name = '$app_name' AND resource_type = 'port' AND service_name LIKE '%webui%' AND status = 'active' ORDER BY service_name LIMIT 1;" 2>/dev/null | cut -d':' -f1)
|
||||
fi
|
||||
local host="${public_ip_v4:-localhost}"
|
||||
# Direct host-port URL: prefer the LAN address. $public_ip_v4 is the WAN
|
||||
# address from an external resolver, and LibrePortal never forwards ports —
|
||||
# so http://<wan-ip>:<port> is unreachable for the LAN/VPN clients this
|
||||
# branch exists to serve, and bakes an off-network host into the app's own
|
||||
# links. Falls back to the old value only if the route lookup found nothing.
|
||||
local host="${local_ip_v4:-${public_ip_v4:-localhost}}"
|
||||
if [[ -n "$external_port" ]]; then
|
||||
app_url="http://$host:$external_port"
|
||||
else
|
||||
|
||||
@ -78,6 +78,64 @@ _instanceSetSubdomain() {
|
||||
updateConfigOption "$key" "$newval" "$file" >/dev/null
|
||||
}
|
||||
|
||||
# Rewrite the parent-service column (field 1) of every PORT_<n> row in the cloned
|
||||
# config so it names the service _instanceRewriteCompose actually stamped into the
|
||||
# compose.
|
||||
#
|
||||
# portAllocate stores this value as the port row's `parent_service`, and the WebUI
|
||||
# joins ports to an app's Docker services on it (webui_services.sh,
|
||||
# db_list_installed_app.sh) — matching against service names read back from the
|
||||
# compose's SERVICE_TAG_<n>. The config re-namespace only rewrites KEYS, so an
|
||||
# instance kept the TYPE's service name in the value while its compose moved on:
|
||||
# the join found nothing and the instance rendered in the WebUI with no port, no
|
||||
# URL and no login row, despite being up and reachable.
|
||||
#
|
||||
# MUST mirror rule 1 of _instanceRewriteCompose — same two token forms, so the
|
||||
# config and the compose can't drift apart. Scoped to PORT_ lines so a bare app
|
||||
# name elsewhere in the config (titles, descriptions, the subdomain column) is
|
||||
# left alone.
|
||||
_instanceRewriteConfigPorts() {
|
||||
local type="$1" slug="$2" file="$3"
|
||||
[[ -f "$file" ]] || return 0
|
||||
sed -i -E "/^CFG_[A-Z0-9_]+_PORT_[0-9]+=/{
|
||||
s/\b${type}-service\b/${slug}-service/g
|
||||
s/\b${type}_db\b/${slug}_db/g
|
||||
}" "$file"
|
||||
}
|
||||
|
||||
# Force every one of the instance's ports to LAN-only: access `private`, Traefik
|
||||
# off. Used by `instance create --local`, for a second copy that should be reached
|
||||
# at http://<ip>:<port> and never given a router or a certificate.
|
||||
#
|
||||
# `disabled` ports are left as-is — the user opted them out of host exposure
|
||||
# entirely, and flipping them to private would start publishing them.
|
||||
_instanceSetLocalOnly() {
|
||||
local slug_u="$1" file="$2"
|
||||
local key line val newval
|
||||
local -a f
|
||||
local n
|
||||
for n in {1..20}; do
|
||||
key="CFG_${slug_u}_PORT_${n}"
|
||||
line=$(grep -E "^${key}=" "$file" | head -n1)
|
||||
[[ -z "$line" ]] && continue
|
||||
val="${line#*=}"
|
||||
val="${val//$'\r'/}"
|
||||
val="${val#\"}"
|
||||
val="${val%\"}"
|
||||
local IFS='|'
|
||||
f=($val)
|
||||
unset IFS
|
||||
while [[ ${#f[@]} -lt 11 ]]; do f+=(""); done
|
||||
[[ "${f[3]}" == "disabled" ]] && continue
|
||||
f[3]="private" # access
|
||||
f[6]="false" # traefik
|
||||
local IFS='|'
|
||||
newval="${f[*]}"
|
||||
unset IFS
|
||||
updateConfigOption "$key" "$newval" "$file" >/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
# Rewrite identity-bearing tokens in the cloned compose so the instance's
|
||||
# containers, Traefik routers and backup labels are unique. image: lines are
|
||||
# deliberately left untouched (rule 2/3 anchor on their line prefix; the
|
||||
@ -146,9 +204,15 @@ _instanceRewriteTools() {
|
||||
}
|
||||
|
||||
# Provision and install a new instance of a multi-instance-capable app.
|
||||
# instanceCreate <type> <name> [domain_index] [subdomain]
|
||||
# instanceCreate <type> <name> [domain_index] [subdomain] [local_only]
|
||||
#
|
||||
# local_only=true skips Traefik entirely — the instance is served on its own
|
||||
# published host port and nothing else. That is also what you get implicitly when
|
||||
# no CFG_DOMAIN_<n> is configured (initializeAppVariables forces the same thing),
|
||||
# so the flag exists for the case where a domain IS set but this particular
|
||||
# instance should stay off it.
|
||||
instanceCreate() {
|
||||
local type="$1" rawname="$2" domain_idx="$3" subdomain="$4"
|
||||
local type="$1" rawname="$2" domain_idx="$3" subdomain="$4" local_only="$5"
|
||||
|
||||
local type_dir="${install_containers_dir%/}/$type"
|
||||
if [[ -z "$type" || ! -d "$type_dir" || ! -f "$type_dir/$type.config" ]]; then
|
||||
@ -180,7 +244,11 @@ instanceCreate() {
|
||||
# Default the host to a hyphen-safe form of the slug; let the caller override.
|
||||
[[ -z "$subdomain" ]] && subdomain="${slug//_/-}"
|
||||
|
||||
isNotice "Creating new '$type' instance '$id' (slug: $slug, host: ${subdomain}.<domain>)"
|
||||
if [[ "$local_only" == "true" ]]; then
|
||||
isNotice "Creating new '$type' instance '$id' (slug: $slug, LAN-only on a published port)"
|
||||
else
|
||||
isNotice "Creating new '$type' instance '$id' (slug: $slug, host: ${subdomain}.<domain>)"
|
||||
fi
|
||||
|
||||
# 1. Clone the type's template tree into a new instance template.
|
||||
local inst_dir="${install_containers_dir%/}/$slug"
|
||||
@ -220,9 +288,17 @@ instanceCreate() {
|
||||
[[ -n "$domain_idx" ]] && _instanceSetCfg "CFG_${slug_u}_DOMAIN" "$domain_idx" "$cfg"
|
||||
_instanceSetSubdomain "$slug_u" "$subdomain" "$cfg"
|
||||
|
||||
# 4. Make the cloned compose + tools target the instance's own identity.
|
||||
_instanceRewriteCompose "$type" "$slug" "$inst_dir"
|
||||
_instanceRewriteTools "$type" "$slug" "$inst_dir"
|
||||
# 4. Make the cloned compose + config ports + tools target the instance's own
|
||||
# identity. The config ports must follow the compose or the WebUI can't
|
||||
# join the instance's ports to its services.
|
||||
_instanceRewriteCompose "$type" "$slug" "$inst_dir"
|
||||
_instanceRewriteConfigPorts "$type" "$slug" "$cfg"
|
||||
_instanceRewriteTools "$type" "$slug" "$inst_dir"
|
||||
|
||||
if [[ "$local_only" == "true" ]]; then
|
||||
_instanceSetLocalOnly "$slug_u" "$cfg"
|
||||
isNotice "Instance '$slug' is LAN-only — no Traefik router, reachable on its published host port."
|
||||
fi
|
||||
|
||||
isSuccessful "Instance template ready: $slug (instance of $type)"
|
||||
|
||||
|
||||
@ -146,6 +146,23 @@ initializeAppVariables()
|
||||
healthcheck=true
|
||||
fi
|
||||
|
||||
# No domain configured -> no Traefik, whatever the per-port column says.
|
||||
# LibrePortal is LAN/VPN-first: a box with CFG_DOMAIN_<n> unset must still
|
||||
# serve every app on http://<ip>:<port>. Left alone, a traefik=true port with
|
||||
# an empty $domain_full stamps Host(`<app>.`) — a trailing-dot host that
|
||||
# matches nothing — and drags APP_URL to https://<app>. with it, breaking any
|
||||
# app that builds its links from APP_URL (Bookstack, Nextcloud, Mastodon).
|
||||
# Forcing the column false makes tagsProcessorPortRouterBlocks comment the
|
||||
# router out entirely and lets tagsProcessorAppUrl fall through to the
|
||||
# http://<ip>:<port> branch. The published host port is unaffected — access
|
||||
# type, not the traefik flag, decides whether a port is allocated.
|
||||
if [[ -z "$domain_full" ]]; then
|
||||
local _nd
|
||||
for _nd in "${!port_traefik_managed[@]}"; do
|
||||
port_traefik_managed[$_nd]="false"
|
||||
done
|
||||
fi
|
||||
|
||||
# $public is derived from the port config — true iff any of this app's
|
||||
# PORT_<n> rows have field 7 (traefik) == "true". Replaces the legacy
|
||||
# CFG_<APP>_PUBLIC field, which was redundant with the per-port flag.
|
||||
@ -174,4 +191,10 @@ initializeAppVariables()
|
||||
host_setup="${_sub}.${domain_full}"
|
||||
fi
|
||||
fi
|
||||
# Every branch above suffixes $domain_full, so with no domain configured they
|
||||
# all yield a bare trailing dot ("bookstack."). That string is not a host, and
|
||||
# it reaches DOMAINSUBNAME_TAG and the trusted-domains list regardless of the
|
||||
# Traefik flag — so blank it rather than let a non-resolving name ship. Both
|
||||
# consumers already treat empty as "no canonical host".
|
||||
[[ -z "$domain_full" ]] && host_setup=""
|
||||
}
|
||||
|
||||
@ -583,8 +583,10 @@ declare -gA LP_FN_MAP=(
|
||||
[instanceList]="instance/instance_create.sh"
|
||||
[instanceRemove]="instance/instance_create.sh"
|
||||
[_instanceRewriteCompose]="instance/instance_create.sh"
|
||||
[_instanceRewriteConfigPorts]="instance/instance_create.sh"
|
||||
[_instanceRewriteTools]="instance/instance_create.sh"
|
||||
[_instanceSetCfg]="instance/instance_create.sh"
|
||||
[_instanceSetLocalOnly]="instance/instance_create.sh"
|
||||
[_instanceSetSubdomain]="instance/instance_create.sh"
|
||||
[instanceTypeCfg]="instance/instance_create.sh"
|
||||
[_invidiousBcrypt]="invidious/scripts/invidious_auth.sh"
|
||||
@ -1731,8 +1733,10 @@ declare -gA LP_FN_ROOT=(
|
||||
[instanceList]="scripts"
|
||||
[instanceRemove]="scripts"
|
||||
[_instanceRewriteCompose]="scripts"
|
||||
[_instanceRewriteConfigPorts]="scripts"
|
||||
[_instanceRewriteTools]="scripts"
|
||||
[_instanceSetCfg]="scripts"
|
||||
[_instanceSetLocalOnly]="scripts"
|
||||
[_instanceSetSubdomain]="scripts"
|
||||
[instanceTypeCfg]="scripts"
|
||||
[_invidiousBcrypt]="containers"
|
||||
@ -2915,8 +2919,10 @@ instanceIdPart() { unset -f instanceIdPart; __lpAutoload "${install_scripts_dir}
|
||||
instanceList() { unset -f instanceList; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceList "$@"; }
|
||||
instanceRemove() { unset -f instanceRemove; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceRemove "$@"; }
|
||||
_instanceRewriteCompose() { unset -f _instanceRewriteCompose; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteCompose "$@"; }
|
||||
_instanceRewriteConfigPorts() { unset -f _instanceRewriteConfigPorts; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteConfigPorts "$@"; }
|
||||
_instanceRewriteTools() { unset -f _instanceRewriteTools; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceRewriteTools "$@"; }
|
||||
_instanceSetCfg() { unset -f _instanceSetCfg; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceSetCfg "$@"; }
|
||||
_instanceSetLocalOnly() { unset -f _instanceSetLocalOnly; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceSetLocalOnly "$@"; }
|
||||
_instanceSetSubdomain() { unset -f _instanceSetSubdomain; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; _instanceSetSubdomain "$@"; }
|
||||
instanceTypeCfg() { unset -f instanceTypeCfg; __lpAutoload "${install_scripts_dir}instance/instance_create.sh"; instanceTypeCfg "$@"; }
|
||||
_invidiousBcrypt() { unset -f _invidiousBcrypt; __lpAutoload "${install_containers_dir}invidious/scripts/invidious_auth.sh"; _invidiousBcrypt "$@"; }
|
||||
|
||||
15
variables.sh
15
variables.sh
@ -34,6 +34,21 @@ fi
|
||||
if [[ -z "$public_ip_v4" ]]; then
|
||||
public_ip_v4="localhost"
|
||||
fi
|
||||
|
||||
# This host's LAN address — the source IP the kernel picks for the default route.
|
||||
# Deliberately separate from $public_ip_v4 above, which prefers the WAN address an
|
||||
# external resolver sees. Anything advertising a *directly published host port*
|
||||
# (APP_URL, printed logins, the WebUI's service URLs) must use this one: LibrePortal
|
||||
# never port-forwards, so the WAN address only ever resolves for someone who set up
|
||||
# forwarding by hand, while the LAN/VPN address is what clients actually dial.
|
||||
local_ip_v4="$(ip -4 route get 1.1.1.1 2>/dev/null | grep -Po '(?<=src )(\S+)' | head -1)"
|
||||
if [[ -z "$local_ip_v4" ]]; then
|
||||
local_ip_v4=$(hostname -I | awk '{print $1}' 2>/dev/null)
|
||||
fi
|
||||
if [[ -z "$local_ip_v4" ]]; then
|
||||
local_ip_v4="localhost"
|
||||
fi
|
||||
|
||||
server_nic="$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1)"
|
||||
default_subnet="10.100.0"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user