Scope app container operations by compose project, not name substring
`docker ps -f name=<app>` is a SUBSTRING match, and instance slugs are <type>_<id> — so the base app's name is a prefix of every instance of it. `name=bookstack` also selected bookstack_home, bookstack_test and their -db containers, which meant start, stop, restart and remove all silently operated on every instance of an app instead of the one named. Worst of the four is remove: `libreportal app remove bookstack` ran `docker rm` against its instances' containers too. Multi-instance made this reachable — the naming scheme it introduced is exactly what turns the base name into a prefix. Each app and instance is already its own compose project, named for its directory, so the project label addresses exactly the containers belonging to that app. app_install.sh's own post-install check already used this label; the lifecycle operations did not. Found while tracing the IP allocation problem: bookstack_work had vanished, and checking how uninstall selects containers turned this up. To be clear about attribution — this bug does NOT explain that disappearance. The log shows an explicit uninstall of bookstack_work, including its own install folder and log, which container-level over-matching cannot do. I could not attribute that removal to a specific command and am not going to guess; the instance has been recreated. Verified: with the fix, `libreportal app stop bookstack` stops bookstack and bookstack-db and leaves bookstack_home and bookstack_test running. Before it, all six went down. All four Bookstack apps and Stoat serve 200 afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d09b21eec1
commit
0a6ea95b08
@ -11,9 +11,16 @@ dockerRemoveApp()
|
||||
isNotice "Stopping and removing Docker containers for '$app_name'. Please wait..."
|
||||
|
||||
# Stop and remove containers in one go
|
||||
local result; result=$(dockerCommandRun "docker ps -aqf name=$app_name | xargs -r docker stop" >/dev/null 2>&1)
|
||||
# Scope by compose project, NOT by name. `docker ps -f name=<app>` is a
|
||||
# SUBSTRING match, so with multi-instance apps the base app's name is a prefix
|
||||
# of every instance of it: `name=bookstack` also matched bookstack_home and
|
||||
# bookstack_test (and their -db containers), so an operation aimed at one app
|
||||
# silently hit all of its instances. Each app and instance is its own compose
|
||||
# project (named for its directory), so the project label addresses exactly
|
||||
# the containers that belong to this app and nothing else.
|
||||
local result; result=$(dockerCommandRun "docker ps -aq --filter label=com.docker.compose.project=$app_name | xargs -r docker stop" >/dev/null 2>&1)
|
||||
checkSuccess "Stopped Docker containers matching '$app_name'"
|
||||
|
||||
local result; result=$(dockerCommandRun "docker ps -aqf name=$app_name | xargs -r docker rm" >/dev/null 2>&1)
|
||||
local result; result=$(dockerCommandRun "docker ps -aq --filter label=com.docker.compose.project=$app_name | xargs -r docker rm" >/dev/null 2>&1)
|
||||
checkSuccess "Removed Docker containers matching '$app_name'"
|
||||
}
|
||||
|
||||
@ -34,7 +34,14 @@ dockerRestartApp()
|
||||
isNotice "Restarting Docker containers for '$app_name'. Please wait..."
|
||||
|
||||
# Restart containers in one go
|
||||
local result; result=$(dockerCommandRun "docker ps -aqf name=$app_name | xargs -r docker restart" >/dev/null 2>&1)
|
||||
# Scope by compose project, NOT by name. `docker ps -f name=<app>` is a
|
||||
# SUBSTRING match, so with multi-instance apps the base app's name is a prefix
|
||||
# of every instance of it: `name=bookstack` also matched bookstack_home and
|
||||
# bookstack_test (and their -db containers), so an operation aimed at one app
|
||||
# silently hit all of its instances. Each app and instance is its own compose
|
||||
# project (named for its directory), so the project label addresses exactly
|
||||
# the containers that belong to this app and nothing else.
|
||||
local result; result=$(dockerCommandRun "docker ps -aq --filter label=com.docker.compose.project=$app_name | xargs -r docker restart" >/dev/null 2>&1)
|
||||
checkSuccess "Restarted Docker containers matching '$app_name'"
|
||||
|
||||
# App-specific restart hook — host-installed apps define restart<App> to
|
||||
|
||||
@ -11,6 +11,13 @@ dockerStartApp()
|
||||
isNotice "Starting Docker containers for '$app_name'. Please wait..."
|
||||
|
||||
# Start containers in one go
|
||||
local result; result=$(dockerCommandRun "docker ps -aqf name=$app_name | xargs -r docker start" >/dev/null 2>&1)
|
||||
# Scope by compose project, NOT by name. `docker ps -f name=<app>` is a
|
||||
# SUBSTRING match, so with multi-instance apps the base app's name is a prefix
|
||||
# of every instance of it: `name=bookstack` also matched bookstack_home and
|
||||
# bookstack_test (and their -db containers), so an operation aimed at one app
|
||||
# silently hit all of its instances. Each app and instance is its own compose
|
||||
# project (named for its directory), so the project label addresses exactly
|
||||
# the containers that belong to this app and nothing else.
|
||||
local result; result=$(dockerCommandRun "docker ps -aq --filter label=com.docker.compose.project=$app_name | xargs -r docker start" >/dev/null 2>&1)
|
||||
checkSuccess "Started Docker containers matching '$app_name'"
|
||||
}
|
||||
|
||||
@ -11,7 +11,14 @@ dockerStopApp()
|
||||
isNotice "Stopping Docker containers for '$app_name'. Please wait..."
|
||||
|
||||
# Stop containers in one go
|
||||
local result; result=$(dockerCommandRun "docker ps -aq --filter name=${app_name} | xargs -r docker stop" >/dev/null 2>&1)
|
||||
# Scope by compose project, NOT by name. `docker ps -f name=<app>` is a
|
||||
# SUBSTRING match, so with multi-instance apps the base app's name is a prefix
|
||||
# of every instance of it: `name=bookstack` also matched bookstack_home and
|
||||
# bookstack_test (and their -db containers), so an operation aimed at one app
|
||||
# silently hit all of its instances. Each app and instance is its own compose
|
||||
# project (named for its directory), so the project label addresses exactly
|
||||
# the containers that belong to this app and nothing else.
|
||||
local result; result=$(dockerCommandRun "docker ps -aq --filter label=com.docker.compose.project=${app_name} | xargs -r docker stop" >/dev/null 2>&1)
|
||||
checkSuccess "Stopped Docker containers matching '$app_name'"
|
||||
|
||||
# App-specific stop hook — host-installed apps define stop<App> to stop
|
||||
|
||||
@ -393,6 +393,20 @@ webuiUpdaterScan() {
|
||||
local compose="$containers_dir/$app/docker-compose.yml"
|
||||
[ -f "$compose" ] || continue
|
||||
local anchor; anchor="$(updaterPrimaryImage "$app" "$compose")"
|
||||
|
||||
# Every reuse below carries a fact the registry told us about the
|
||||
# ANCHOR. If the anchor has since become a different image, those facts
|
||||
# describe the old one and must not be reused. This is not theoretical:
|
||||
# fixing stoat's anchor moved it from mongo:8.0 to ghcr.io/stoatchat/api,
|
||||
# and its "last rebuilt" date stayed MongoDB's — byte-identical to
|
||||
# library/mongo's — so the unmaintained check was judging Stoat by
|
||||
# MongoDB's release cadence. Reuse is for a throttled scan, not for an
|
||||
# app whose identity changed underneath it.
|
||||
local prev_anchor="" prev_same=0
|
||||
if [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
prev_anchor="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.current_image)//""' "$prev_json" 2>/dev/null)"
|
||||
[ "$prev_anchor" = "$anchor" ] && prev_same=1
|
||||
fi
|
||||
local channel; channel="$(updaterTagOf "$anchor")"; [ -n "$channel" ] || channel="latest"
|
||||
local vtype; vtype="$(updaterClassifyTag "$channel")"
|
||||
|
||||
@ -413,10 +427,14 @@ webuiUpdaterScan() {
|
||||
# manual-only, so an explicit "never check on its own" is honoured.
|
||||
local app_registry="$do_registry"
|
||||
if [ "$app_registry" = "0" ] && [ "$reg_interval" != "0" ] && [ "$have_jq" = "1" ]; then
|
||||
# An anchor that has CHANGED image counts as never-answered too:
|
||||
# the stored digest belongs to the image we no longer run, so there
|
||||
# is nothing valid to reuse and waiting out the window would leave
|
||||
# the app described by its predecessor.
|
||||
local _seen=""
|
||||
[ -f "$prev_json" ] && _seen="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.available_digest)//""' "$prev_json" 2>/dev/null)"
|
||||
[ "$prev_same" = "1" ] && _seen="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.available_digest)//""' "$prev_json" 2>/dev/null)"
|
||||
[ "$_seen" = "null" ] && _seen=""
|
||||
[ -z "$_seen" ] && app_registry=1 # never answered (new app, or first scan)
|
||||
[ -z "$_seen" ] && app_registry=1 # never answered, or a new anchor
|
||||
fi
|
||||
|
||||
# available (registry) digest for the anchor's channel
|
||||
@ -425,7 +443,7 @@ webuiUpdaterScan() {
|
||||
avail_dig="$(updaterRegistryDigest "$(updaterRepoTag "$anchor")")"
|
||||
fi
|
||||
# reuse the prior value when we didn't (or couldn't) reach the registry
|
||||
if [ -z "$avail_dig" ] && [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
if [ -z "$avail_dig" ] && [ "$prev_same" = "1" ]; then
|
||||
avail_dig="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.available_digest)//""' "$prev_json" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
@ -439,7 +457,7 @@ webuiUpdaterScan() {
|
||||
if [ "$app_registry" = "1" ]; then
|
||||
img_updated="$(updaterTagLastUpdated "$(updaterRepoTag "$anchor")" "$channel")"
|
||||
fi
|
||||
if [ -z "$img_updated" ] && [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
if [ -z "$img_updated" ] && [ "$prev_same" = "1" ]; then
|
||||
img_updated="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.image_updated_at)//""' "$prev_json" 2>/dev/null)"
|
||||
[ "$img_updated" = "null" ] && img_updated=""
|
||||
fi
|
||||
@ -453,7 +471,7 @@ webuiUpdaterScan() {
|
||||
if [ "$app_registry" = "1" ]; then
|
||||
newer_ver="$(updaterNewerVersionTag "$channel" "$(updaterRepoTag "$anchor")")"
|
||||
newer_ver="${newer_ver%%:*}"
|
||||
elif [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
elif [ "$prev_same" = "1" ]; then
|
||||
newer_ver="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.newer_version)//""' "$prev_json" 2>/dev/null)"
|
||||
[ "$newer_ver" = "null" ] && newer_ver=""
|
||||
fi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user