#!/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. pathIsContainerData "$work_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 }