Recreate containers when a deploy rebuilds their image
podman-compose only recreates containers when the compose file's own hash changes. A push that only changed code built a new image, but the running containers kept the old one. Deploys now build first and pass --force-recreate when a container's image ID no longer matches its tag. Compose calls also pass -p with the app's name. Without it, compose named the project after the compose file's directory, so every git app (stacks/<app>/repo/compose.yaml) was the project "repo". That broke the terminal and monitoring checks, and --remove-orphans could remove another git app's containers. On its next deploy or restart, an app's containers from the old project are removed and recreated under the new name. An app whose containers use named volumes keeps the old project (recorded as APP_COMPOSE_PROJECT), since its volumes are named after it; for those apps panelctl skips --remove-orphans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbWSNkXxZhYf7eqHTyx3Bf
This commit is contained in:
parent
d9d0136d70
commit
0cf5c4bc02
1 changed files with 81 additions and 7 deletions
88
panelctl.sh
88
panelctl.sh
|
|
@ -17,6 +17,7 @@ PANEL_GROUP="${PANEL_GROUP:-panelroutes}"
|
|||
# as KEY=VALUE words (passed to compose via env(1), never sourced).
|
||||
CURRENT_APP=""
|
||||
COMPOSE_ARGS=()
|
||||
ORPHAN_ARGS=()
|
||||
APP_ENV_ARGS=()
|
||||
APP_ENV_KEYS=()
|
||||
|
||||
|
|
@ -198,6 +199,7 @@ load_app() {
|
|||
local manifest
|
||||
manifest="$(app_manifest "${name}")"
|
||||
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
|
||||
unset APP_COMPOSE_PROJECT
|
||||
# shellcheck disable=SC1090
|
||||
source "${manifest}"
|
||||
|
||||
|
|
@ -235,10 +237,20 @@ app_ports_override() {
|
|||
echo "${STACKS_DIR}/$1/.panel-ports.yaml"
|
||||
}
|
||||
|
||||
# The compose project the app's containers belong to: the app's name, unless
|
||||
# the manifest keeps an older one (see adopt_legacy_project).
|
||||
compose_project() {
|
||||
echo "${APP_COMPOSE_PROJECT:-${CURRENT_APP}}"
|
||||
}
|
||||
|
||||
# The app's compose file followed by whichever generated overrides exist.
|
||||
refresh_compose_args() {
|
||||
local override
|
||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}")
|
||||
COMPOSE_ARGS=(-p "$(compose_project)" -f "${APP_COMPOSE_FILE}")
|
||||
# Other git apps may share a kept legacy project; their containers would
|
||||
# look like orphans.
|
||||
ORPHAN_ARGS=(--remove-orphans)
|
||||
[[ "$(compose_project)" == "${CURRENT_APP}" ]] || ORPHAN_ARGS=()
|
||||
for override in "$(app_ports_override "${CURRENT_APP}")" "$(app_env_override "${CURRENT_APP}")"; do
|
||||
if [[ -f "${override}" ]]; then
|
||||
COMPOSE_ARGS+=(-f "${override}")
|
||||
|
|
@ -597,6 +609,56 @@ cmd_render_route() {
|
|||
log info "rendered route ${route_file}"
|
||||
}
|
||||
|
||||
# Before panelctl passed -p, compose named the project after the compose
|
||||
# file's directory, so every git app (repo/compose.yaml) shared the project
|
||||
# "repo". Move the app's old containers out of the way so they are recreated
|
||||
# under its own name. Named volumes are prefixed with the project name, so an
|
||||
# app whose containers use them keeps its old project instead.
|
||||
adopt_legacy_project() {
|
||||
[[ -z "${APP_COMPOSE_PROJECT:-}" ]] || return 0
|
||||
local dir id project legacy="" volumes
|
||||
local ids=()
|
||||
dir="$(cd "$(dirname "${APP_COMPOSE_FILE}")" 2>/dev/null && pwd -P)" || return 0
|
||||
|
||||
while IFS='|' read -r id project; do
|
||||
[[ -n "${id}" && -n "${project}" && "${project}" != "${CURRENT_APP}" ]] || continue
|
||||
ids+=("${id}")
|
||||
legacy="${project}"
|
||||
done < <(run_podman ps --all --filter "label=com.docker.compose.project.working_dir=${dir}" \
|
||||
--format '{{.ID}}|{{index .Labels "com.docker.compose.project"}}' 2>/dev/null || true)
|
||||
[[ ${#ids[@]} -gt 0 ]] || return 0
|
||||
|
||||
volumes="$(run_podman inspect --format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}} {{end}}{{end}}' "${ids[@]}" 2>/dev/null | xargs || true)"
|
||||
if [[ -n "${volumes}" ]]; then
|
||||
echo "APP_COMPOSE_PROJECT=\"${legacy}\"" >>"$(app_manifest "${CURRENT_APP}")"
|
||||
APP_COMPOSE_PROJECT="${legacy}"
|
||||
refresh_compose_args
|
||||
echo "Keeping compose project '${legacy}', because the app's containers use named volumes (${volumes})"
|
||||
log warning "app '${CURRENT_APP}' keeps compose project '${legacy}' because its containers use named volumes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Moving the app's containers from compose project '${legacy}' to '${CURRENT_APP}'"
|
||||
run_podman rm --force "${ids[@]}" >/dev/null || fail "could not remove the old containers"
|
||||
# The shared pod goes away once no other app's containers are left in it.
|
||||
run_podman pod rm "pod_${legacy}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# True when a container of the app runs an older image than its tag now
|
||||
# points at, e.g. after a rebuild. podman-compose only recreates containers
|
||||
# when the compose file itself changes, so without this a push that only
|
||||
# changes code would build a new image and keep running the old one.
|
||||
containers_outdated() {
|
||||
local id image current
|
||||
while IFS='|' read -r id image; do
|
||||
[[ -n "${id}" && -n "${image}" ]] || continue
|
||||
current="$(run_podman image inspect --format '{{.Id}}' "${image}" 2>/dev/null)" || continue
|
||||
[[ "${current}" == "${id}"* || "${id}" == "${current}"* ]] || return 0
|
||||
done < <(run_podman ps --all --filter "label=com.docker.compose.project=$(compose_project)" \
|
||||
--format '{{.ImageID}}|{{.Image}}' 2>/dev/null || true)
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_deploy() {
|
||||
local name="$1"
|
||||
validate_name "${name}"
|
||||
|
|
@ -607,10 +669,21 @@ cmd_deploy() {
|
|||
cmd_render_route "${name}"
|
||||
prepare_ports_override "${name}"
|
||||
prepare_env_override "${name}"
|
||||
adopt_legacy_project
|
||||
|
||||
# Stream compose output as it happens (the panel shows it live in the
|
||||
# deployment log) and keep a copy in the journal.
|
||||
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1 | tee >(journal_copy); then
|
||||
if ! run_compose "${COMPOSE_ARGS[@]}" build 2>&1 | tee >(journal_copy); then
|
||||
log err "Deployment failed for app '${name}'"
|
||||
fail "image build failed"
|
||||
fi
|
||||
|
||||
local up_args=(up -d --no-build "${ORPHAN_ARGS[@]}")
|
||||
if containers_outdated; then
|
||||
echo "Images changed, recreating the app's containers"
|
||||
up_args+=(--force-recreate)
|
||||
fi
|
||||
if ! run_compose "${COMPOSE_ARGS[@]}" "${up_args[@]}" 2>&1 | tee >(journal_copy); then
|
||||
log err "Deployment failed for app '${name}'"
|
||||
fail "compose up failed"
|
||||
fi
|
||||
|
|
@ -625,11 +698,12 @@ cmd_restart() {
|
|||
|
||||
log info "Restarting app '${name}'"
|
||||
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed"
|
||||
adopt_legacy_project
|
||||
run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" || fail "compose down failed"
|
||||
prepare_ports_override "${name}"
|
||||
prepare_env_override "${name}"
|
||||
|
||||
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1; then
|
||||
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build "${ORPHAN_ARGS[@]}" 2>&1; then
|
||||
fail "compose up failed during restart"
|
||||
fi
|
||||
|
||||
|
|
@ -641,7 +715,7 @@ cmd_stop() {
|
|||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed"
|
||||
run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" || fail "compose down failed"
|
||||
log info "stopped app '${name}'"
|
||||
}
|
||||
|
||||
|
|
@ -742,7 +816,7 @@ cmd_remove() {
|
|||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" 2>/dev/null || true
|
||||
|
||||
# Remove this app's block from the aggregate routes file.
|
||||
local route_file
|
||||
|
|
@ -878,7 +952,7 @@ cmd_volume_clear() {
|
|||
load_app "${name}"
|
||||
|
||||
log info "clearing volume data for app '${name}'"
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" 2>/dev/null || true
|
||||
|
||||
local data_dir="${APP_VOLUME_DIR}/data"
|
||||
if [[ -d "${data_dir}" ]]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue