#!/bin/bash # ============================================================================================== # ============================= Docker Update — Remaining ====================================== # ============================================================================================== # Pulls the latest image for every running container NOT already covered by the daily or # weekly update/restart cycles. Runs at the end of the weekly maintenance window. # # ── WHAT THIS COVERS ────────────────────────────────────────────────────────────────────────── # Daily update: DAILY_RESTART_CONTAINERS — auth stack, NPM, Dispatcharr, etc. # Weekly update: WEEKLY_RESTART_CONTAINERS — NextCloud, AdGuard, Immich, etc. # This script: everything else running on the system (media stack, utilities, etc.) # # Together the three scripts ensure every deployed container receives at least one image # pull per week, with no container list to maintain here — it derives the remainder # automatically from `docker ps` minus the two managed lists. # # ── EXCLUSION LOGIC ─────────────────────────────────────────────────────────────────────────── # Exclusion set = DAILY_RESTART_CONTAINERS + WEEKLY_RESTART_CONTAINERS (aliased by detect_hosts) # Only running containers are targeted — stopped containers are intentionally excluded # (stopped = likely paused intentionally; pulling while stopped adds no value). # # ── TOGGLE ──────────────────────────────────────────────────────────────────────────────────── # WEEKLY_REMAINING_UPDATES=false in master.conf — skips all pulls, exits cleanly # # ── CONFIGURATION ───────────────────────────────────────────────────────────────────────────── # master.conf: WEEKLY_REMAINING_UPDATES — enable/disable (default: true) # master_host*.conf: HOST*_DAILY_RESTART_CONTAINERS — excluded from this script # HOST*_WEEKLY_RESTART_CONTAINERS — excluded from this script # # ── USAGE ───────────────────────────────────────────────────────────────────────────────────── # docker_update_remaining.sh — normal run # docker_update_remaining.sh --dry-run — show which containers would be pulled # docker_update_remaining.sh --log — verbose output # docker_update_remaining.sh --status — show config and exit # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" parse_args "$@" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi if ! command -v docker &>/dev/null; then error "Docker command not found" exit 1 fi detect_hosts if [[ "${WEEKLY_REMAINING_UPDATES:-true}" != "true" ]]; then log "WEEKLY_REMAINING_UPDATES=false — skipping remaining container updates" exit 0 fi # ── Build exclusion set from daily + weekly managed lists ───────────────────────────────────── declare -A EXCLUDED for c in "${DAILY_RESTART_CONTAINERS[@]}" "${WEEKLY_RESTART_CONTAINERS[@]}"; do [[ -n "$c" ]] && EXCLUDED["$c"]=1 done # ── Get all running containers ──────────────────────────────────────────────────────────────── mapfile -t ALL_RUNNING < <(docker ps --format '{{.Names}}' 2>/dev/null | sort) # ── Derive remainder: running minus excluded ────────────────────────────────────────────────── REMAINING=() for c in "${ALL_RUNNING[@]}"; do [[ -z "$c" ]] && continue [[ -n "${EXCLUDED[$c]:-}" ]] && continue REMAINING+=("$c") done # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_GEAR Enabled: ${WEEKLY_REMAINING_UPDATES:-true}" echo "$ICON_CONTAINERS All running: ${#ALL_RUNNING[@]}" echo "$ICON_CONTAINERS Excluded: ${!EXCLUDED[*]}" echo "$ICON_CONTAINERS Remaining: ${REMAINING[*]:-none}" echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi if [[ ${#REMAINING[@]} -eq 0 ]]; then log "No remaining containers to update — all running containers are covered by daily/weekly lists" exit 0 fi [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no images will be pulled" # ============================================================================================== # ━━━ Pull Updates ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_CONTAINERS Docker Update (Remaining) — $(date '+%Y-%m-%d %H:%M:%S') ━━━" echo "$ICON_CONTAINERS Containers: ${REMAINING[*]}" echo "$ICON_CONTAINERS Excluded (managed elsewhere): ${!EXCLUDED[*]}" echo "" START=$(date +%s) UPDATED=() UP_TO_DATE=() FAILED=() for container in "${REMAINING[@]}"; do [[ -z "$container" ]] && continue echo "━━━ $ICON_CONTAINERS $container ━━━" IMAGE=$(docker inspect --format='{{.Config.Image}}' "$container" 2>/dev/null) if [[ -z "$IMAGE" ]]; then warn "$container — could not determine image, skipping" FAILED+=("$container") echo "" continue fi log "$container — image: $IMAGE" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would pull: $IMAGE" UPDATED+=("$container") echo "" continue fi OLD_ID=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "") echo "$ICON_SYNC Pulling $IMAGE..." if docker pull "$IMAGE" 2>&1 | grep -E "^(Status:|Digest:|Error|error)" | sed 's/^/ /'; then NEW_ID=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "") if [[ -n "$OLD_ID" ]] && [[ "$OLD_ID" != "$NEW_ID" ]]; then echo "$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 echo "" done END=$(date +%s) # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINING) SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" echo "$ICON_CONTAINERS Scope: ${#ALL_RUNNING[@]} running — ${#EXCLUDED[@]} managed = ${#REMAINING[@]} checked" [[ ${#UPDATED[@]} -gt 0 ]] && echo "$ICON_DONE Updated: ${UPDATED[*]}" [[ ${#UP_TO_DATE[@]} -gt 0 ]] && echo "$ICON_RUNNING Up to date: ${UP_TO_DATE[*]}" [[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no images pulled" elif [[ ${#FAILED[@]} -eq 0 ]]; then log "$ICON_DONE Status: done ✅ — ${#UPDATED[@]} updated, ${#UP_TO_DATE[@]} current" else warn "Status: ${#FAILED[@]} pull(s) failed — containers continue on existing images" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Pull failures are non-fatal exit 0