diff --git a/scripts/docker/command/stranded_containers.sh b/scripts/docker/command/stranded_containers.sh new file mode 100644 index 0000000..96c2f37 --- /dev/null +++ b/scripts/docker/command/stranded_containers.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# A containers-root wipe (fresh install, or a relocation of the containers root) +# removes an app's project directory but NOT the container the daemon still holds +# in its own state. Those containers are stranded: their compose file, config and +# bind-mount sources are gone, so they can never start correctly again. +# +# They are not inert. `restart: unless-stopped` makes the daemon retry them on +# every restart, and Docker materialises each missing bind-mount source before +# handing off to runc — creating an empty DIRECTORY even where the mount is a +# file. That is how a fresh install ends up with .config as a directory: the +# install restarts the rootless daemon, the daemon resurrects the previous +# install's container, and the stubs it leaves behind collide with the install's +# own file copies moments later. +# +# Sweep them once the daemon is up, so the next restart has nothing to resurrect. +# Scoped to containers whose project directory sits under the LibrePortal +# containers root — unrelated containers on the host are never touched. +dockerRemoveStrandedContainers() +{ + local silent_flag="${1:-silent}" + + [[ -d "$containers_dir" ]] || return 0 + + # `systemctl restart` returns once the unit is active, which can be before + # dockerd finishes initialising. Give the socket a bounded moment to answer. + local attempt=0 + until runFileOp docker info >/dev/null 2>&1; do + attempt=$((attempt + 1)) + if [[ "$attempt" -ge 15 ]]; then + [[ "$silent_flag" == "loud" ]] && isNotice "Docker not responding — skipping stranded container sweep." + return 0 + fi + sleep 2 + done + + local listing + listing=$(runFileOp docker ps -a --format '{{.Names}} {{.Label "com.docker.compose.project.working_dir"}}' 2>/dev/null) + [[ -z "$listing" ]] && return 0 + + local removed=0 name work_dir + while IFS=$'\t' read -r name work_dir; do + [[ -z "$name" || -z "$work_dir" ]] && continue + # Only LibrePortal's own container tree, and only when the project + # directory is provably gone. + [[ "$work_dir" == "$containers_dir"* ]] || continue + [[ -d "$work_dir" ]] && continue + + if runFileOp docker rm -f "$name" >/dev/null 2>&1; then + removed=$((removed + 1)) + [[ "$silent_flag" == "loud" ]] && isNotice "Removed stranded container '$name' (project dir $work_dir is gone)." + fi + done <<< "$listing" + + if [[ "$removed" -gt 0 ]]; then + isSuccessful "Removed $removed stranded container(s) left without a project directory" + fi +} diff --git a/scripts/docker/install/rootless/rootless_docker.sh b/scripts/docker/install/rootless/rootless_docker.sh index da427e6..c54ec54 100755 --- a/scripts/docker/install/rootless/rootless_docker.sh +++ b/scripts/docker/install/rootless/rootless_docker.sh @@ -183,6 +183,12 @@ EOL" local result; result=$(dockerCommandRunInstallUser "systemctl --user restart docker") checkSuccess "Reload the systemd user docker service" + # The restart above runs the daemon's container-restore pass, which + # resurrects containers left over from a previous install whose project + # directory this install already wiped. Clear them now so the next + # restart can't re-create their bind-mount sources as stub directories. + dockerRemoveStrandedContainers "loud" + local result; result=$(sudo cp $sysctl $sysctl.bak) checkSuccess "Backing up sysctl file" diff --git a/scripts/source/files/arrays/files_docker.sh b/scripts/source/files/arrays/files_docker.sh index 26685fa..9424e56 100755 --- a/scripts/source/files/arrays/files_docker.sh +++ b/scripts/source/files/arrays/files_docker.sh @@ -31,6 +31,7 @@ docker_scripts=( "docker/command/docker_run_install.sh" "docker/command/docker_run.sh" "docker/command/run_privileged.sh" + "docker/command/stranded_containers.sh" "docker/compose/copy_build_context.sh" "docker/compose/restart_after_update.sh" "docker/compose/setup_compose_yml.sh" diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 2ba6287..8ca5345 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -411,6 +411,7 @@ declare -gA LP_FN_MAP=( [dockerPruneAppNetworks]="docker/network/network_prune.sh" [dockerRemoveApp]="docker/app/docker/remove_app.sh" [dockerRemoveAppImages]="docker/app/uninstall/remove_images.sh" + [dockerRemoveStrandedContainers]="docker/command/stranded_containers.sh" [dockerRestartApp]="docker/app/docker/restart_app.sh" [dockerRestartAppViaInstall]="docker/app/functions/function_restart_app.sh" [dockerServiceStart]="docker/service/start_docker.sh" @@ -1416,6 +1417,7 @@ declare -gA LP_FN_ROOT=( [dockerPruneAppNetworks]="scripts" [dockerRemoveApp]="scripts" [dockerRemoveAppImages]="scripts" + [dockerRemoveStrandedContainers]="scripts" [dockerRestartApp]="scripts" [dockerRestartAppViaInstall]="scripts" [dockerServiceStart]="scripts" @@ -2454,6 +2456,7 @@ dockerInstallApp() { unset -f dockerInstallApp; __lpAutoload "${install_scripts_ dockerPruneAppNetworks() { unset -f dockerPruneAppNetworks; __lpAutoload "${install_scripts_dir}docker/network/network_prune.sh"; dockerPruneAppNetworks "$@"; } dockerRemoveApp() { unset -f dockerRemoveApp; __lpAutoload "${install_scripts_dir}docker/app/docker/remove_app.sh"; dockerRemoveApp "$@"; } dockerRemoveAppImages() { unset -f dockerRemoveAppImages; __lpAutoload "${install_scripts_dir}docker/app/uninstall/remove_images.sh"; dockerRemoveAppImages "$@"; } +dockerRemoveStrandedContainers() { unset -f dockerRemoveStrandedContainers; __lpAutoload "${install_scripts_dir}docker/command/stranded_containers.sh"; dockerRemoveStrandedContainers "$@"; } dockerRestartApp() { unset -f dockerRestartApp; __lpAutoload "${install_scripts_dir}docker/app/docker/restart_app.sh"; dockerRestartApp "$@"; } dockerRestartAppViaInstall() { unset -f dockerRestartAppViaInstall; __lpAutoload "${install_scripts_dir}docker/app/functions/function_restart_app.sh"; dockerRestartAppViaInstall "$@"; } dockerServiceStart() { unset -f dockerServiceStart; __lpAutoload "${install_scripts_dir}docker/service/start_docker.sh"; dockerServiceStart "$@"; }