portFindNextAvailablePort consulted LibrePortal's own network_resources table
plus a hardcoded list (8080, 6060) and CFG_RESERVED_PORTS_EXTRA — while the
comment above it claimed a picked port "can never collide with a host service at
compose-up time". It can: the list only covers what someone thought to write
down. Ask the kernel instead, via ss, read once per allocation rather than per
candidate. No ss => empty set => exactly the old behaviour.
Found while restoring 13 apps onto a desktop, though not the cause there:
stoat's livekit publishes a FIXED udp range (50000-50100, which it advertises to
clients and so cannot be re-rolled), and kdeconnectd held 50016. That collision
needs its own answer; this fixes the randomly-allocated ports, which had the
same exposure with no reason to.
Also make the bulk restore stop reporting a half-running app as a clean success.
continue-on-error lets a failed compose-up log and carry on, so restoreAppStart
returns 0 either way — which is how that run printed "13 apps restored" while
four of stoat's containers had exited 101. checkSuccess already appends every
failure to error_report.log, so watch it grow across each app and name the ones
that were noisy.
scripts/dev/lp-port-host-test binds a real socket and asserts the allocator
refuses that port; verified it fails when the check is removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Instance install (bugs found by running one end to end):
- The cloned compose kept the TYPE's tag namespace
(#LIBREPORTAL|BOOKSTACK_APP_KEY_1_TAG|...) while the config had been
re-namespaced to CFG_<SLUG>_*, so tagsProcessorAppConfigValues matched
nothing, the placeholders survived and the pre-start guard refused to
launch. Rewrite the tag names and *_DATA tokens too — narrowly, so an
app whose compose sets a real env var named after itself is untouched.
- Tools/hooks kept uppercase CFG_<TYPE>_ reads, so an instance
provisioned itself from the type's config and ignored its own values.
- Cloned hooks were never loaded: both loaders run at startup, before the
instance dir exists, so _appCallHook's `declare -F` found nothing and
every <slug>_install_* hook silently no-opped — for bookstack that is
the readiness probe and the admin bootstrap. Source the instance's own
scripts in-process, then regen arrays + manifest for later runs.
- bookstack's hook hardcoded the container name after `docker exec -e ...`
flags, where the rewriter can't see it, so an instance's admin bootstrap
ran against the BASE app's container — including a tinker DELETE of a
user. Target "$app_name" instead, and teach the rewriter the
container="<type>" assignment form used by auth adapters.
network_resources uniqueness:
UNIQUE(resource_type, resource_value) is right for 'ip' and 'port' but the
port-tag writer stores descriptive rows in the same table with INSERT OR
REPLACE, so every install DELETED the matching row from whichever app held
it. traefik_managed and url_accessible are booleans, so the whole table
could only ever hold one row of each. Observed live: installing a second
bookstack took all four traefik_managed/url_accessible rows from stoat and
bookstack, and removing that instance took the stolen rows with it.
Replace it with a partial unique index scoped to ip/port, and migrate
existing databases in place (SQLite can't drop a constraint, so the table
is rebuilt inside a transaction). The migration is invoked from
portUpdateComposeTags, not just databaseCreateTables — the latter only
runs from startPreInstall, which a working install never re-runs.
Verified: two bookstacks now hold port_tag_internal=80, traefik_managed
and url_accessible simultaneously; duplicate host ports and IPs are still
rejected; instance installs, serves HTTP 200, provisions its own admin in
its own database, and removes cleanly with no orphan rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bookstack: create-admin fails on a reinstall because the account is
already provisioned. That took the generic-failure branch, which printed
the upstream defaults (admin@admin.com / password) as "the" login — those
credentials were replaced on the first install, so the one line a user
would act on was the wrong one. Detect "already exists" and say the
existing account was kept and its password not reset.
Firewall: uninstall deleted only resource_type='port', orphaning the
port_tag_*/traefik_managed/url_accessible rows the rebuild reads. Every
rebuild then walked ports for long-gone apps and printed "Skipped: <app>
(app not found)" per row. Widen the uninstall delete to all non-'ip' rows
(the source), and prune already-orphaned rows in the rebuild (the
self-heal). Pruning requires both no container dir AND status != 1 in the
apps table, so a mid-flight install can't prune itself.
Verified on a live bookstack reinstall: admin path reports correctly,
firewall pruned nextcloud + stalwart once, second run silent, 11 rules
added / 0 failed throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Foundations for network-drift healing:
- ipInSubnet(ip, cidr): prefix-aware CIDR membership (pure bash), so
stored IPs can be checked against docker's real subnet. Honours the
actual prefix, so a healthy /16-subnet + /24-ip-range install is not
mistaken for drift.
- dockerInstallApp now accepts reset_network="ip": re-roll the static IP
from the current subnet but PRESERVE published host ports (clears only
IP rows; LIBREPORTAL_RESET_IP_ONLY keeps port_allocate reusing existing
ports). This is the heal path — a subnet move strands the IP, not the
port, so we don't churn bookmarks/forwards/proxy upstreams. reset="true"
still re-rolls both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'local result=$(cmd)' resets $? to 0 (the local builtin's own exit), so the
following checkSuccess always saw success regardless of cmd's real exit — the
mechanism that masked the de-sudo write failures. Split declaration from
assignment ('local result; result=$(cmd)') across all 235 active-code sites
(84 files) so the command's exit reaches checkSuccess. No behaviour change
beyond $? now being accurate (no set -e in runtime code; multi-line
assignments transform safely).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The apps SQLite DB ($docker_dir/$db_file) is owned by the manager user, so
read/write it AS the manager via runInstallOp instead of sudo (root). 48 call
sites across 28 scripts. In rooted this drops root->manager (correct owner);
in rootless it's the manager too (using runFileOp/dockerinstall here was the
'unable to open database' bug). The broken 'command -v sudo sqlite3' check
lines are left untouched (separate pre-existing issue).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Reinstall test on Debian 12 surfaced three rootless-only breakages (rooted
was byte-identical/fine):
1. pasta blocked by Debian's passt AppArmor profile (DENIED ptrace read ->
can't open container netns -> rootless dockerd never starts). Default
CFG_ROOTLESS_NET back to slirp4netns (reliable); pasta stays selectable
for hosts that relax the profile.
2. de-sudo mis-assigned helpers by owner. /docker management layer (apps DB
chowned to libreportal by install_sqlite, /docker/logs) is MANAGER-owned,
not dockerinstall. Add runInstallWrite; move apps-DB sqlite3 -> runInstallOp
and /docker/logs appends -> runInstallWrite. Revert ownership-SETUP scripts
(libreportal_folders, app_folder) to runSystem — they must run as root to
establish ownership during install. Container files (/docker/containers/<app>)
stay runFileOp.
3. kernel hardening sysctls written to /etc/sysctl/99-custom.conf, which
'sysctl --system' does not read -> never applied. Write them to
/etc/sysctl.d/99-libreportal-hardening.conf instead.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
All operate on /docker data-plane (DB at $docker_dir, compose files,
task dir /docker/.../frontend/data/tasks): sqlite3/find/sed/mkdir/chmod/
chown/mv/rm/mkfifo/truncate/install/tee -> runFileOp/runFileWrite. The
two systemctl enable/start calls in the check processor -> runSystem.
Dropped spurious sudo on text-only echo/grep/date in db_app_scan.
Byte-identical in rooted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.
Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>