The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by actually installing it. The compose bind-mounts
./resources/nginx.conf into the web container, but nothing ever copied
that file into the container tree, so Docker created a DIRECTORY in its
place and nginx died with:
error mounting ".../resources/nginx.conf" to rootfs at
"/etc/nginx/nginx.conf": not a directory
Worse than a hard failure: the app still recorded as installed. Three of
four containers came up, the DB and the app itself were fine, and only
the web front end was missing — a quiet, partial install.
Apps needing a resource file declare the copy in a hook (authelia does
exactly this); Nextcloud simply never had one. Adds
nextcloud_install_post_compose — after the compose file is written,
before permissions and `up` — which repairs any stub directory left by a
previous attempt and then copies the file.
The stub repair matters: without it the copy lands INSIDE the directory
(resources/nginx.conf/nginx.conf) and the mount fails identically.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stepping 31 -> 32 -> 33 is arithmetic. Knowing 32 FINISHED before
touching 33 is the whole safety story, and it is invisible from outside
the app: Nextcloud runs its migration on boot and sits in maintenance
mode — or fails halfway — while Docker reports the container perfectly
healthy. Advance a rung there and a migration has been skipped on live
data.
Contract: <app>_upgrade_verify <app> <expected-tag> <deadline> -> 0
Returns 0 ONLY on positive confirmation that the app serves at the
expected version with nothing outstanding. Unhealthy, indeterminate and
timed-out all return non-zero — uncertainty is a failure, not a maybe,
because the alternative gambles with data.
nextcloud `occ status`: installed, NOT in maintenance, no pending DB
upgrade, and the running major matches the tag. Maintenance
mid-migration is expected and simply keeps waiting.
mastodon /health serving, ZERO "down" rows in db:migrate:status, and
the version from /api/v1/instance matching. /health alone is
insufficient — Puma answers before migrations finish.
stalwart /healthz/ready (per its documented probes), required to hold
stable rather than flash once. Weaker by design: the probes
confirm serving but report no version, and the file says so
rather than implying more.
updaterVerifyGeneric (running + healthy + no restart during a settle
window) is the fallback for everything else, and is explicitly NOT
sufficient to justify climbing a rung — the engine will refuse to ladder
an app with no declared verifier.
9 tests drive the dangerous states directly: maintenance mode, pending DB
upgrade, and a wrong major all correctly REFUSE to verify; clean states
pass. Those three negatives are the ones that would have corrupted data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three closeouts in one pass:
1. DEVELOPMENT.md — consolidated hook-conventions table covering all 8 per-app
hook types (tools / update-specifics / compose-tags / webui-refresh / the two
traefik markers / the two network-provider hooks). One place to look instead
of inferring from the codebase.
2. Nextcloud APCu wired alongside Redis: appUpdateSpecifics_nextcloud now sets
memcache.local=\OC\Memcache\APCu too (was deferred from the fpm switch). APCu
= cheap in-process cache; the fpm-alpine image ships the extension. CLI mode
may emit a harmless "no memory cache" notice on `occ` runs — Nextcloud is
graceful, the FPM worker still uses APCu fine.
3. Container-side file-capture rollout to 3 confident cases:
- bookstack: lscr.io/linuxserver/bookstack with PUID=1000 → /config (1000:1000)
- gitea: gitea/gitea with USER_UID=1000 → /data (1000:1000)
- owncloud: owncloud/server (Apache/PHP) → /mnt/data (33:33, www-data)
Snapshots are now complete for these (the dir's excluded from the raw restic
pass and captured live through the container as a tar → libreportal-owned
staging, same proven pattern as Nextcloud). Less-evidenced candidates left
for live verification: linkding, mastodon, jellyfin, trilium, focalboard,
invidious, vaultwarden, headscale-service — each needs its in-container uid
confirmed before labeling (wrong uid won't break backup, but restore would
chown to the wrong owner).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Drop Apache+mod_php for the actual performance win — nginx + PHP-FPM — without
the LinuxServer image cascade (custom auto-install, /custom-cont-init.d, abc-vs-
www-data rewrites in the auth adapter + every tool, HTTPS-by-default quirks).
The official fpm-alpine image keeps env-var auto-install and the www-data user,
so the auth adapter, all tools, and the compose-tags hook keep working unchanged.
- Compose: nextcloud-service is now fpm-alpine (still container_name=nextcloud-
service so docker exec ... nextcloud-service php occ in the auth adapter is
untouched). New nextcloud-web nginx sidecar serves :80 over the shared ./html
volume, terminating FastCGI to nextcloud-service:9000. Traefik labels + PORTS_
TAG_1 move to nextcloud-web (the HTTP face); backup.files stays on -service
(the file-owning brain). nextcloud-db + nextcloud-redis unchanged.
- resources/nginx.conf: Nextcloud's recommended nginx config, trimmed for
behind-Traefik (no TLS), large-upload + caldav/carddav/.well-known redirects.
- scripts/nextcloud_update_specifics.sh: NEW post-install hook —
appUpdateSpecifics_nextcloud waits for first-boot occ install to complete
(config.php + occ status=installed), then wires Redis as memcache.distributed
+ memcache.locking via occ config:system:set. Idempotent.
Auto-install is unchanged (official image's NEXTCLOUD_ADMIN_USER + MYSQL_* env
flow). Redis caching now actually USED by Nextcloud (previously the container
was up but config.php had no memcache config). Container-side backup capture
still the right answer for the perm boundary — image change doesn't affect it.
Verified statically: yaml structure, hook parses + dispatches + has the right
graceful-timeout fallback when occ isn't reachable. Live verification (sync
performance + actual Redis hit rate + traefik proxy of FastCGI) needs a fresh
install on a throwaway box.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
docker_config_setup_data.sh's "App Specific" if/elif ladder (pihole, nextcloud,
searxng, speedtest, vaultwarden, wireguard, gluetun) becomes a generic hook
dispatch: an app needing computed (non-CFG) compose tags ships
containers/<app>/scripts/<app>_compose_tags.sh defining appSetupComposeTags_<app>
(live-sourced by the container scan, called with the compose path; reads
host_setup/public_ip_v4/CFG_* from scope). Same declare -F pattern as the tool /
update-specifics / webui-refresh hooks.
- 7 per-app hook files added; central ladder replaced by the dispatch.
- The generic gluetun network-mode block stays (any app may route through gluetun);
tagsProcessorGluetunForwardedPorts stays central (hook + network-mode both use it).
- Regenerate arrays (hooks live under containers/, not arrayed).
Verified with stubs: each hook emits exactly the tags the old branch did
(pihole REV_SERVER, nextcloud trusted-domains, gluetun VPN set + forwarded ports,
etc.); apps without a hook are a clean no-op.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Each app now carries everything under containers/<app>/: Tools-tab actions in
tools/ (declaration <app>.tools.json + function <app>_<tool_id>.sh) and logic
helpers in scripts/ (e.g. <app>_auth.sh). The container scan live-sources every
.sh under the app (maxdepth 3, prunes only resources/) and webui_tools.sh
auto-merges the .tools.json, so an app is a true drop-in — no central edit, no
array regen.
- Empty the central webui_tools.sh heredoc; all 34 tools across 11 apps now
come from per-app declarations (verified byte-identical to the old output).
- Retire the orphaned mattermost tool scripts to scripts/unused (there is no
containers/mattermost; its install fn already lived in unused).
- Update the dispatch comment/error path, the auth-adapter doc, and
DEVELOPMENT.md to the new convention.
- Regenerate static arrays (files_app.sh no longer lists app/containers/*).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>