Files
Varaverk/Docker_Essentials/docker_update.sh
T
Gmer4LfeandClaude Sonnet 4.6 fdba23c32d fix: use rebuild_container instead of docker restart after image pulls
docker restart uses the image ID baked in at container creation — it never
picks up a new digest. rebuild_container reads the stored Unraid XML template,
stops the old container, recreates it with the new image, then handles the
old image cleanup. This is what the Unraid UI 'Apply Update' button does.

docker_update.sh: add rebuild section after pull loop for DAILY containers.
docker_update_remaining.sh: replace retry_docker docker restart with rebuild_container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 15:09:55 -04:00

386 lines
16 KiB
Bash

#!/bin/bash
# ==============================================================================================
# ================================= Docker Update ==============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Pulls the latest images for configured containers. Two modes: normal (daily)
# and remainder (weekly).
#
# Normal mode is called by daily_sync_maintenance.sh before docker_daily_restart.sh.
# Containers stay running during the pull — no extra downtime beyond what the
# nightly restart already causes.
#
# Remainder mode is called by weekly_sync_maintenance.sh as the final update step.
# It catches everything that normal mode and the weekly sync window did not already
# update — derived automatically from docker ps, nothing to configure.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Normal mode (daily):
# Targets DAILY_RESTART_CONTAINERS — same list used by docker_daily_restart.sh.
# Pull → compare old vs new image ID → mark updated or already current.
# docker_daily_restart.sh runs after — containers restart onto the fresh image.
#
# Remainder mode (weekly):
# Targets all currently running containers NOT in:
# DAILY_RESTART_CONTAINERS — already updated daily
# emby + critical-data profiles — updated inline by the weekly sync window
# FALLBACK_*_TIER* — owned by the remote server's update cycle
# Pull → compare → prune dangling images.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Single List
# Normal mode reuses DAILY_RESTART_CONTAINERS rather than maintaining a
# separate update list. Adding or removing a container from the restart list
# automatically updates the image pull list — one change, both places.
#
# Version Ownership
# Fallback containers are excluded from remainder mode. This server only runs
# them during a failover. The remote server owns their version — if remainder
# updates them independently and a handback occurs, the remote's older image
# may not handle data written by the newer version.
#
# State Respect
# Stopped containers are never targeted. Pulling while stopped adds no value
# and a stopped container was likely halted intentionally.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Lock Acquisition
# Prevents concurrent execution via acquire_lock(). Safe to call from
# maintenance scripts without risk of overlap.
#
# Host Detection
# detect_hosts() identifies which server is running the script and aliases
# HOST*_DAILY_RESTART_CONTAINERS to the correct host's values.
#
# Root Enforcement
# Docker operations require root privileges.
#
# DAILY_CONTAINER_UPDATES Toggle
# Normal mode exits cleanly when disabled. docker_daily_restart.sh still runs
# regardless — update and restart are independent operations.
#
# Fallback Exclusion
# Remainder mode excludes containers owned by the remote server's update cycle
# to prevent version divergence across the failover boundary.
#
# Running-Only Filter
# Stopped containers excluded from remainder mode — intentionally down.
#
# Image ID Comparison
# Containers not restarted unless their image actually changed. Pulls that
# result in "already up to date" produce no restart.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# DAILY_CONTAINER_UPDATES
# Enable or disable normal mode. docker_daily_restart.sh runs regardless.
# (default: true)
#
# PROFILE_CRITICAL_CONTAINER_NAMES[emby|critical-data]
# Container names for emby and critical-data profiles — excluded from
# remainder mode (already updated by the weekly sync window)
#
# host*.conf
#
# HOST*_DAILY_RESTART_CONTAINERS
# Containers updated in normal mode. Aliased by detect_hosts() →
# DAILY_RESTART_CONTAINERS
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_update.sh
# Normal mode — pull latest images for DAILY_RESTART_CONTAINERS
#
# docker_update.sh --remainder
# Remainder mode — pull all running containers not in managed lists,
# restart those that received updates, prune dangling images
#
# docker_update.sh --dry-run
# Preview which containers would be pulled without making changes
#
# docker_update.sh --status
# Show configuration and container list for current mode
#
# docker_update.sh --log
# Verbose per-container pull and comparison output
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
# Pre-parse --remainder before parse_args (unknown args pass through to PARSED_ARGS)
REMAINDER_MODE=false
_filtered_args=()
for _arg in "$@"; do
if [[ "$_arg" == "--remainder" ]]; then
REMAINDER_MODE=true
else
_filtered_args+=("$_arg")
fi
done
unset _arg
parse_args "${_filtered_args[@]}"
unset _filtered_args
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
acquire_lock
if ! command -v docker &>/dev/null; then
error "Docker command not found"
exit 1
fi
detect_hosts
# ==============================================================================================
# ━━━ Container Discovery ━━━
# ==============================================================================================
if [[ "$REMAINDER_MODE" == true ]]; then
declare -A _exclude=()
# Daily containers — updated by docker_update.sh normal mode
for _c in "${DAILY_RESTART_CONTAINERS[@]}"; do
[[ -n "$_c" ]] && _exclude["$_c"]=1
done
# Weekly sync-window containers (emby + critical-data) — updated inline by weekly_sync_maintenance.sh
_weekly_str="${PROFILE_CRITICAL_CONTAINER_NAMES[emby]:-} ${PROFILE_CRITICAL_CONTAINER_NAMES[critical-data]:-}"
read -r -a _weekly_arr <<< "$_weekly_str"
for _c in "${_weekly_arr[@]}"; do
[[ -n "$_c" ]] && _exclude["$_c"]=1
done
unset _weekly_str _weekly_arr
# Fallback coverage containers — owned by the remote server's update cycle.
# This server runs them during failover but should never update them independently.
# Updating them here risks version divergence: if remote's writeback after handback
# encounters data written by a newer version, it may not handle it correctly.
for _tier in 1 2 3 4; do
_tier_var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER${_tier}"
eval "_tier_arr=(\"\${${_tier_var}[@]:-}\")" 2>/dev/null
for _c in "${_tier_arr[@]}"; do
[[ -n "$_c" ]] && _exclude["$_c"]=1
done
done
unset _tier _tier_var _tier_arr _c
mapfile -t _all_running < <(docker ps --format '{{.Names}}' | sort)
TARGET_CONTAINERS=()
for _c in "${_all_running[@]}"; do
[[ -z "${_exclude[$_c]+x}" ]] && TARGET_CONTAINERS+=("$_c")
done
unset _all_running _exclude _c
else
if [[ "${DAILY_CONTAINER_UPDATES:-true}" != "true" ]]; then
echo "DAILY_CONTAINER_UPDATES=false — skipping container updates"
exit 0
fi
if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
exit 0
fi
TARGET_CONTAINERS=("${DAILY_RESTART_CONTAINERS[@]}")
fi
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Mode: $([[ "$REMAINDER_MODE" == true ]] && echo "remainder" || echo "normal (daily)")"
if [[ "$REMAINDER_MODE" == true ]]; then
echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly sync, and fallback)"
for _c in "${TARGET_CONTAINERS[@]}"; do echo " $_c"; done
else
echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
echo "$ICON_GEAR Enabled: ${DAILY_CONTAINER_UPDATES:-true}"
fi
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no images will be pulled"
if [[ ${#TARGET_CONTAINERS[@]} -eq 0 ]]; then
echo "No containers to update"
exit 0
fi
# ==============================================================================================
# ━━━ Pull Updates ━━━
# ==============================================================================================
echo ""
if [[ "$REMAINDER_MODE" == true ]]; then
echo "━━━ $ICON_CONTAINERS Docker Update (remainder) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily or weekly sync)"
else
echo "━━━ $ICON_CONTAINERS Docker Update — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
fi
echo ""
START=$(date +%s)
UPDATED=()
UP_TO_DATE=()
FAILED=()
SKIPPED=()
for container in "${TARGET_CONTAINERS[@]}"; do
[[ -z "$container" ]] && continue
log "━━━ $ICON_CONTAINERS $container ━━━"
if ! docker inspect "$container" &>/dev/null; then
warn "$container — not found, skipping"
SKIPPED+=("$container")
continue
fi
IMAGE=$(docker inspect --format='{{.Config.Image}}' "$container" 2>/dev/null)
if [[ -z "$IMAGE" ]]; then
warn "$container — could not determine image, skipping"
SKIPPED+=("$container")
continue
fi
log "$container — image: $IMAGE"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would pull: $IMAGE"
UPDATED+=("$container")
continue
fi
# Capture image ID before pull to detect whether an update landed
OLD_ID=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "")
log "$ICON_SYNC Pulling $IMAGE..."
if [[ "$ENABLE_LOGGING" == "true" ]]; then
docker pull "$IMAGE" 2>&1 | grep -E "^(Status:|Digest:|Error|error)" | sed 's/^/ /'
_pull_rc=${PIPESTATUS[0]}
else
docker pull "$IMAGE" >/dev/null 2>&1
_pull_rc=$?
fi
NEW_ID=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "")
if [[ $_pull_rc -eq 0 ]]; then
if [[ -n "$OLD_ID" ]] && [[ "$OLD_ID" != "$NEW_ID" ]]; then
log "$ICON_DONE $container — updated ✅"
UPDATED+=("$container")
else
log "$container — already up to date"
UP_TO_DATE+=("$container")
fi
else
warn "$container — pull failed ($IMAGE)"
FAILED+=("$container")
fi
done
# ── Recreate containers that received a new image ────────────────────────────
# docker restart uses the image ID baked in at creation time — it never picks
# up the new digest. rebuild_container reads the stored XML template, stops the
# old container, recreates it (new image, same config), then prunes the old image.
REBUILT=()
REBUILD_FAILED=()
if [[ ${#UPDATED[@]} -gt 0 ]]; then
for container in "${UPDATED[@]}"; do
[[ -z "$container" ]] && continue
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would rebuild $container from template"
REBUILT+=("$container")
continue
fi
log "$ICON_SYNC Rebuilding $container from template on new image..."
if /usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container "$container" >/dev/null 2>&1; then
log "$ICON_DONE $container rebuilt ✅"
REBUILT+=("$container")
else
error "Failed to rebuild $container — will be picked up by docker_daily_restart.sh"
notify "$container failed to rebuild after image update on $(hostname)" "Docker Update" "warning"
REBUILD_FAILED+=("$container")
fi
done
fi
# ── Prune dangling images ─────────────────────────────────────────────────────
# Old images become dangling after a pull lands a new digest. Prune here so
# they don't accumulate across daily runs.
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would prune dangling images"
PRUNED_SUMMARY="(dry run)"
else
PRUNED_OUTPUT=$(docker image prune -f 2>&1)
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$PRUNED_OUTPUT" | sed 's/^/ /'
PRUNED_SUMMARY=$(echo "$PRUNED_OUTPUT" | grep -E "^Total reclaimed" || echo "nothing reclaimed")
fi
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
if [[ "$REMAINDER_MODE" == true ]]; then
echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINDER) SUMMARY ━━━━━"
else
echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE SUMMARY ━━━━━"
fi
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
if [[ ${#UPDATED[@]} -gt 0 ]]; then
echo "$ICON_DONE Updated: ${#UPDATED[@]}"
log " ${UPDATED[*]}"
fi
[[ ${#REBUILT[@]} -gt 0 ]] && echo "$ICON_SYNC Rebuilt: ${#REBUILT[@]}"
[[ ${#REBUILD_FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Rebuild fail:${REBUILD_FAILED[*]}"
[[ ${#UP_TO_DATE[@]} -gt 0 ]] && log "$ICON_RUNNING Up to date: ${#UP_TO_DATE[@]}"
[[ ${#SKIPPED[@]} -gt 0 ]] && log "$ICON_WARN Skipped: ${#SKIPPED[@]}"
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
echo "$ICON_SYNC Pruned: ${PRUNED_SUMMARY:-none}"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no images pulled"
elif [[ ${#FAILED[@]} -eq 0 ]]; then
echo "$ICON_DONE Status: done ✅ — ${#UPDATED[@]} updated, ${#UP_TO_DATE[@]} current"
else
warn "Status: ${#FAILED[@]} pull(s) failed — restart will proceed with existing images"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Pull failures are non-fatal — restart proceeds regardless
exit 0