Two small uninstall-output tweaks.
1. dockerComposeDownRemove now ALWAYS calls dockerRemoveApp (the
`docker ps -aqf name=…` → stop + rm sweep) as a fallback, even when
the compose-down step is skipped because the app dir is missing.
Before, a partial prior uninstall (compose file gone but containers
still running) produced "App directory not found. Skipping container
shutdown." and then proceeded as if the uninstall were complete —
leaving the actual containers running. The name-based sweep also
runs after a successful compose-down to catch anything compose
wouldn't pick up (renamed services, orphans from earlier failures).
While here: the OS_TYPE gate (only Ubuntu/Debian) is gone too —
`docker compose down` works on any OS with docker, and gating it
meant Arch/etc. users got NO compose teardown at all.
2. The step-2 header "Keeping Docker images (pass --delete-images to
remove)" trimmed to just "Keeping Docker images". The `isNotice`
line below already explains the reuse-on-reinstall behaviour; the
CLI-flag hint reads as noise in the WebUI task log where users
can't act on it anyway. CLI users can still pass --delete-images
(cli_app_commands.sh wires it as before) or tick the WebUI's
"Also delete docker image" checkbox.
Signed-off-by: librelad <librelad@digitalangels.vip>
dockerDeleteData (uninstall) and the wipe-before-restore step in
restoreAppStart both did `runFileOp rm -rf $containers_dir$app_name`,
which runs as $CFG_DOCKER_INSTALL_USER (dockerinstall, uid 1002 on
rootless). That user owns app-template files but CANNOT remove
container sub-UID dirs created by the daemon's userns mapping —
postgres data at uid 232070, nextcloud html at uid 33, etc. The rm
therefore silently failed with
rm: cannot remove '/libreportal-containers/invidious/postgresdata':
Permission denied
while still reporting "<app> successfully uninstalled" — leaving the
sub-UID directory tree on disk to confuse the next install and leak
storage.
Fix: route the wipe through a new `app-data-remove` action in the
root-owned libreportal-ownership helper. Root can rm sub-UID files
unconditionally. The helper validates the app name (alphanumeric +
. _ -, no traversal), refuses the WebUI's own slot (libreportal), and
is idempotent when the dir is already gone.
Two callers updated:
- scripts/docker/app/uninstall/delete_data.sh
- scripts/restore/restore_app_start.sh
The helper itself ships root-owned at /usr/local/lib/libreportal/, so a
fresh install or release upgrade is needed to pick up the new action.
Bumped init.sh footprint_version 2 → 3 so the runtime updater
prompts a root re-install on the next release.
Signed-off-by: librelad <librelad@digitalangels.vip>
The 31 containers/<app>/<app>.sh files each defined install<App>() with
the SAME 10-step sequence — ~4,000 lines of duplicated boilerplate.
Replaces all that with one generic driver + hook surface.
scripts/app/install/app_install.sh:
installApp <slug> [config_variables]
— Dispatches on $<slug> (c/u/s/r/i) the same way the per-app .sh
files did. Same convention; dockerInstallApp's existing
`declare $app=i` callsite needs no change.
— Runs the standard sequence: dockerConfigSetupToContainer →
dockerComposeSetupFile → optional .env copy → fixPermissions →
dockerComposeUpdateAndStartApp → standard post-install steps
(appUpdateSpecifics, setupHeadscale, databaseInstallApp,
webuiContainerSetup, monitoring registration) → final message.
— Hooks (all declare-f-gated, silent no-op when absent):
<slug>_install_pre / _post_setup / _post_compose / _post_start
<slug>_install_message_data (echoes extra args for menu)
<slug>_install_post
<slug>_uninstall_pre / _post
<slug>_stop_post
<slug>_restart_post
Hooks live in containers/<app>/tools/<app>_tools.sh (auto-sourced
per the modular-per-app-tools convention).
function_install_app.sh:
When no install<App>() function exists, fall through to
`installApp <app_name>` instead of erroring. So an app with no .sh
at all becomes a zero-byte addition — drop in <app>.config +
docker-compose.yml + <app>.svg, done.
containers/linkding/linkding.sh:
Deleted (canary). Linkding's body was 100% standard sequence;
fallback handles it identically. Smoke-tested with stubbed helpers
— dispatcher fires, generic runs full flow, monitoring integration
+ final-message hook plumbing all intact.
Wave B (next): delete the .sh for every other 'pure-boilerplate' app
(~15 candidates per the survey). Wave C: extract custom logic from
the 7 fat apps into hooks before deleting their .sh.
Signed-off-by: librelad <librelad@digitalangels.vip>
If it's gluetun code, it lives with gluetun. Both functions in
scripts/config/tags/processors/tags_processor_network_mode.sh manipulate gluetun
markers / gluetun's compose, so move them into containers/gluetun/scripts/
gluetun_network.sh and rename to the per-app-hook convention:
tagsProcessorNetworkMode -> appNetworkApplyMode_gluetun
tagsProcessorGluetunForwardedPorts -> appNetworkRegisterPorts_gluetun
Central call sites are now provider-agnostic — no "gluetun" literal anywhere:
- docker_config_setup_data.sh: an app routing via CFG_<APP>_NETWORK=<provider>
triggers `appNetworkApplyMode_<provider>` + `appNetworkRegisterPorts_<provider>`
via declare -F, so any future gateway provider plugs in with no engine edits.
- uninstall_app.sh: loops every `appNetworkRegisterPorts_*` hook (each self-skips
when its provider isn't installed), so removing a routed app refreshes the
right provider with no provider name in central code.
Delete tags_processor_network_mode.sh; regenerate arrays. Verified with stubs:
default mode no-ops, gluetun-routed app fires both hooks, gluetun itself is
skipped, unknown provider is silently no-op, uninstall loop calls registerPorts.
Drive-by cleanup: 9 stale "${X_scripts[@]}" array references in app_files.sh /
cli_files.sh (gluetun + headscale from this session's moves, plus 7 pre-existing:
command/ssl/swapfile/ufw/ufwd/user — all from older refactors that left them
behind). Each expanded to nothing at runtime (harmless), but they're dead
misleading refs. Cleaned both files; every remaining array ref now points to a
real files_*.sh.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
dockerCheckAllowedInstall was a one-app `case` whose only active caller was the
wireguard app itself — so inline its check (abort if a host WireGuard exists at
/etc/wireguard/params, which would collide on the wg kernel module + UDP 51820)
directly into containers/wireguard/wireguard.sh and delete
scripts/docker/app/checks/allowed_install.sh.
The protection is unchanged; wireguard is now fully self-contained and the last
app name leaves central install code. Regenerated arrays. (The only remaining
dockerCheckAllowedInstall references are in scripts/unused/ — retired apps,
never sourced.)
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>
Bare `find "$containers_dir"` runs as the manager, but under rootless
containers/ is dockerinstall-owned 751 (traversable, not list-readable by
the manager) -> "find: /docker/containers/: Permission denied". For the
app-log generator that was cosmetic; for dockerComposeUpAllApps /
dockerComposeDownAllApps it silently enumerates nothing so no apps come
up/down. Route these through runFileOp find (dockerinstall in rootless,
manager in rooted — correct in both). The two docker-type switcher finds
are deliberately left: mid-switch the at-rest container owner can differ
from the target-mode user runFileOp resolves to, so they need mode-aware
handling rather than a blind swap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- docker_run: in rooted mode run docker AS the manager via the docker
group (no sudo); the type=='sudo' branch was unreachable dead code
- 8 db helpers: fix 'command -v sudo sqlite3' guard to 'command -v
sqlite3' (bodies already query via runInstallOp)
- restic/kopia single-file dump: write target_file via runBackupOp tee
(as the backup user, matching the snapshot-restore path) instead of
root tee
- adguard auth: root-owned scratch via runSystem mktemp
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Container-plane docker now routes through the mode-aware helpers instead of
sudo: simple calls (exec/ps/run/build/images/inspect/port/logs across ~15
app/check scripts) -> runFileOp docker (rootless socket as the install user;
rooted via the docker group). The cd && docker compose paths drop the sudo on
the rooted branch (the rootless branch already used dockerCommandRunInstallUser
-- byte-identical now, manager-ready later); gluetun, which had no rootless
branch, now uses dockerCommandRun so force-recreate works in both modes.
Co-Authored-By: Claude Opus 4.7 <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>
dockerComposeDown printed the 'Docker Compose down <app>' header then could
fall through silently: when the effective install type (passed type arg or
CFG_DOCKER_INSTALL_TYPE) was empty/unrecognised no branch ran, and on a
non-Ubuntu/Debian OS the whole block was skipped. Collapse the duplicated
type=='' vs type!='' branches into one mode fallback and add notices for the
unknown-mode and unsupported-OS cases so the header always has a result line.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
dockerStartAllApps expanded $(docker ps -a -q) in the outer control-plane
shell, which has no DOCKER_HOST and so hit the nonexistent rooted socket at
/var/run/docker.sock. In rootless mode that connection fails, the
substitution returns empty, and 'docker restart' is then called with no
arguments. Push the whole pipeline into dockerCommandRun (matching
dockerRestartApp) and guard with xargs -r so it runs against the rootless
socket and no-ops cleanly when there are no containers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
dockerInstallApp built the installer name by upper-casing only the first
letter of the slug (libreportal -> installLibreportal), which can't match
camelCase installers like installLibrePortal. After the EasyDocker ->
LibrePortal rename this broke `libreportal` installs with
"installLibreportal: command not found".
If the naive name isn't a defined function, resolve it case-insensitively
against the function table (compgen -A function), and fail with a clear
message if nothing matches. Works for any compound brand/app name.
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>