Files
Varaverk/Docker_Essentials/docker_daily_restart.sh
T
Gmer4Lfe 8be54ab5ee Fix play state sync, add manual stop detection, minor bug fixes
play_state_sync: fix TVDB episode lookup for Jellyfin 10.x — AnyProviderIdEquals
returns the entire library for TVDB queries; switch to season+episode search with
ProviderIds.Tvdb validation to find the correct episode. Also fix pkey extraction
that was pulling s7e2 instead of the TVDB ID from tvdb:ep:5618559:s7e2.

docker_watchdog: add automatic manual-stop detection — containers stopped cleanly
(exit 0/143) are tracked in docker_watchdog_manual_stop.db and skipped until
restarted, removing the need to add manually-stopped containers to the exclusion
list. Auto-clears when the container is seen running again.

docker_daily_restart: remove bare `local` declarations outside a function that
were printing an error for every container restarted.
2026-06-02 17:55:26 -04:00

389 lines
16 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# ================================= Docker Daily Restart =======================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Restarts configured containers every night at 1am as proactive maintenance.
#
# Called by daily_sync_maintenance.sh via DAILY_MAINTENANCE_SCRIPTS. Runs inside
# the daily maintenance window — any service downtime is absorbed by a window
# that is already happening. Also drives docker_update.sh in normal mode: the
# same DAILY_RESTART_CONTAINERS list is used for both restarts and image pulls,
# so there is no second list to maintain.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Proactive Maintenance
# Daily restarts target containers known to degrade over time without
# crossing a clear failure threshold — connection table growth, scheduler
# state accumulation, session cache bloat. The watchdog cannot detect this
# class of degradation. Scheduled restarts clear it before it becomes visible.
#
# State Respect
# Running containers are restarted. Stopped containers are left stopped — they
# were intentionally halted and this script has no authority to override that
# decision. This rule is consistent across the entire ecosystem.
#
# Dependency-Safe Ordering
# Restarts follow the same dependency ordering used by docker_watchdog.sh.
# Services that other containers depend on restart first. A dependent is never
# restarted while its dependency is still coming up.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Dependency Ordering
# Containers restart in dependency-safe order using HOST*_WATCHDOG_DEPENDENCIES.
# CONTAINER_DELAY seconds between dependency restart and dependent restart gives
# the dependency time to fully initialise before dependents try to connect.
#
# Restart Verification
# After each restart, container state is checked after a settle period. A
# container that starts and immediately crashes is marked failed and a
# notification is sent — the script does not silently pass a restart that
# did not stick.
#
# Timeout Protection
# All docker commands wrapped in a 30 second timeout. A hung Docker daemon
# cannot cause this script to hang indefinitely. Timed-out commands retry
# per RETRY_COUNT before marking as failed.
#
# Lock Acquisition
# acquire_lock() prevents concurrent execution if a previous run is still active.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
# HOST*_DAILY_RESTART_CONTAINERS
# Containers restarted nightly. Also used by docker_update.sh normal mode
# for image pulls — add a container once, it gets both. Aliased by
# detect_hosts() → DAILY_RESTART_CONTAINERS
#
# HOST*_WATCHDOG_DEPENDENCIES
# Dependency ordering shared with docker_watchdog.sh. Aliased by
# detect_hosts() → WATCHDOG_DEPENDENCIES
#
# master.conf
#
# RETRY_COUNT
# Retry attempts before giving up on a container
#
# SLEEP
# Seconds between retry attempts
#
# CONTAINER_DELAY
# Seconds to wait after restarting a dependency before starting its dependents
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_daily_restart.sh
# Restart all containers in DAILY_RESTART_CONTAINERS
#
# docker_daily_restart.sh --dry-run
# Preview which containers would be restarted and which would be skipped
#
# docker_daily_restart.sh --status
# Show configured restart list, container states, and dependency ordering
#
# docker_daily_restart.sh --log
# Verbose per-container execution output
#
# ==============================================================================================
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
acquire_lock
if ! command -v docker &>/dev/null; then
error "Docker command not found — check PATH or Docker installation"
notify "Docker daily restart failed — Docker not found on $(hostname)" "Docker Daily Restart" "warning"
exit 1
fi
# detect_hosts() sets MY_ID and aliases HOST*_DAILY_RESTART_CONTAINERS → DAILY_RESTART_CONTAINERS
detect_hosts
if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to restart"
warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
exit 0
fi
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_CONTAINERS Containers: ${DAILY_RESTART_CONTAINERS[*]}"
echo "$ICON_RETRY Retries: $RETRY_COUNT"
echo "$ICON_TIME Sleep: ${SLEEP}s between retries"
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no containers will be restarted"
# ==============================================================================================
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# Wraps docker commands with a 30 second timeout.
# Prevents a hung Docker daemon from causing the script to hang indefinitely.
# Usage: docker_cmd docker restart ContainerName
DOCKER_TIMEOUT=30
docker_cmd() {
timeout "$DOCKER_TIMEOUT" "$@"
local exit_code=$?
if [[ "$exit_code" -eq 124 ]]; then
error "Docker command timed out after ${DOCKER_TIMEOUT}s: $*"
return 1
fi
return "$exit_code"
}
# Verifies a container is still running after restart.
# Gives the container a short settle period before checking.
# Returns 0 if running, 1 if crashed or stopped.
RESTART_VERIFY_WAIT=5 # seconds to wait before checking state post-restart
verify_running() {
local container="$1"
sleep "$RESTART_VERIFY_WAIT"
local state
state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)
if [[ "$state" != "true" ]]; then
error "$container failed to stay running after restart — may have crashed"
return 1
fi
return 0
}
# Builds a dependency-safe restart order from DAILY_RESTART_CONTAINERS.
# Containers that are dependencies of others restart first.
# Returns ordered list in ORDERED_RESTART array.
build_restart_order() {
ORDERED_RESTART=()
local remaining=("${DAILY_RESTART_CONTAINERS[@]}")
local placed=()
# First pass — add dependency containers that appear in our list
for container in "${remaining[@]}"; do
[[ -z "$container" ]] && continue
local is_dependency=false
# Check if this container is a dependency of any other in our list
for dep_string in "${WATCHDOG_DEPENDENCIES[@]}"; do
if [[ "$dep_string" == *"$container"* ]]; then
is_dependency=true
break
fi
done
# Also check associative array format
for dependent in "${!WATCHDOG_DEPENDENCIES[@]}"; do
if [[ "${WATCHDOG_DEPENDENCIES[$dependent]}" == *"$container"* ]]; then
is_dependency=true
break
fi
done
if [[ "$is_dependency" == true ]]; then
# Check not already placed
local already=false
for p in "${placed[@]}"; do [[ "$p" == "$container" ]] && already=true && break; done
if [[ "$already" == false ]]; then
ORDERED_RESTART+=("$container")
placed+=("$container")
fi
fi
done
# Second pass — add remaining containers (dependents and independents)
for container in "${remaining[@]}"; do
[[ -z "$container" ]] && continue
local already=false
for p in "${placed[@]}"; do [[ "$p" == "$container" ]] && already=true && break; done
if [[ "$already" == false ]]; then
ORDERED_RESTART+=("$container")
placed+=("$container")
fi
done
}
# Checks if a container is a dependent of the previously restarted container.
# If so, waits CONTAINER_DELAY before restarting to allow dependency to settle.
# Usage: check_dependency_delay "$container" "$last_restarted"
check_dependency_delay() {
local container="$1"
local last="$2"
[[ -z "$last" ]] && return
local deps="${WATCHDOG_DEPENDENCIES[$container]:-}"
if [[ -n "$deps" ]] && [[ "$deps" == *"$last"* ]]; then
echo " Waiting ${CONTAINER_DELAY}s — $container depends on $last..."
sleep "$CONTAINER_DELAY"
fi
}
# Retries a docker command up to RETRY_COUNT times with SLEEP seconds between attempts.
# Uses docker_cmd wrapper for timeout protection on each attempt.
# Usage: retry_docker docker restart ContainerName
retry_docker() {
local attempt=1
while [[ "$attempt" -le "$RETRY_COUNT" ]]; do
log "$ICON_RETRY Attempt $attempt of $RETRY_COUNT: $*"
if docker_cmd "$@"; then
log "Succeeded on attempt $attempt"
return 0
else
warn "Attempt $attempt failed"
(( attempt++ ))
[[ "$attempt" -le "$RETRY_COUNT" ]] && sleep "$SLEEP"
fi
done
error "Command failed after $RETRY_COUNT attempts: $*"
return 1
}
# ==============================================================================================
# ━━━ Daily Restart ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_CONTAINERS Daily Restart — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) — ${#DAILY_RESTART_CONTAINERS[@]} container(s)"
log "$ICON_CONTAINERS Containers: ${DAILY_RESTART_CONTAINERS[*]}"
log "$ICON_RETRY Retries: $RETRY_COUNT"
log "$ICON_GEAR Config: sleep=${SLEEP}s delay=${CONTAINER_DELAY}s verify-wait=${RESTART_VERIFY_WAIT}s cmd-timeout=${DOCKER_TIMEOUT}s"
START=$(date +%s)
FAILED=()
RESTARTED=()
SKIPPED=()
# Build dependency-safe restart order
build_restart_order
log "$ICON_GEAR Restart order: ${ORDERED_RESTART[*]}"
LAST_RESTARTED=""
for container in "${ORDERED_RESTART[@]}"; do
[[ -z "$container" ]] && continue
c_start=$(date +%s)
c_image=$(docker inspect --format '{{.Config.Image}}' "$container" 2>/dev/null || echo "unknown")
log "━━━ $ICON_CONTAINERS $container ($c_image) ━━━"
if ! timeout "$DOCKER_TIMEOUT" docker inspect "$container" &>/dev/null; then
warn "$container does not exist — skipping"
continue
fi
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)
case "$STATUS" in
true)
log "$ICON_RUNNING $container is running — restarting..."
# Wait if this container depends on the last one restarted
check_dependency_delay "$container" "$LAST_RESTARTED"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would restart $container"
RESTARTED+=("$container")
else
if retry_docker docker restart "$container"; then
# Verify container stayed running after restart
if verify_running "$container"; then
log "$ICON_STARTED $container restarted and running in $(format_duration $(( $(date +%s) - c_start ))) ✅"
RESTARTED+=("$container")
LAST_RESTARTED="$container"
else
error "$container restarted but crashed immediately"
notify "$container crashed after restart on $(hostname)" "Docker Daily Restart" "warning"
FAILED+=("$container")
fi
else
error "Failed to restart $container after $RETRY_COUNT attempts"
notify "$container failed to restart on $(hostname)" "Docker Daily Restart" "warning"
FAILED+=("$container")
fi
fi
;;
false)
log "$ICON_NOT_RUNNING $container is stopped — skipping"
SKIPPED+=("$container")
;;
*)
error "Unknown status for $container: $STATUS"
FAILED+=("$container")
;;
esac
done
END=$(date +%s)
# ==============================================================================================
# ━━━ Prune Old Images ━━━
# ==============================================================================================
# Restarts above swap containers onto new images — old images are now dangling. Prune immediately.
echo ""
echo "━━━ $ICON_SYNC Pruning Dangling Images — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
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
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo "━━━━━ $ICON_SUMMARY DAILY RESTART SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
echo "$ICON_CONTAINERS Scope: ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipped, ${#FAILED[@]} failed"
[[ ${#RESTARTED[@]} -gt 0 ]] && log "$ICON_STARTED Restarted: ${RESTARTED[*]}"
[[ ${#SKIPPED[@]} -gt 0 ]] && log "$ICON_NOT_RUNNING 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 changes made"
elif [[ ${#FAILED[@]} -eq 0 ]]; then
echo "$ICON_DONE Status: ALL DONE ✅"
notify "Daily restart complete — ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipped on $(hostname)" "Docker Daily Restart" "normal"
else
echo "$ICON_ERROR Status: ${#FAILED[@]} container(s) failed"
notify "Daily restart completed with errors on $(hostname) — failed: ${FAILED[*]}" "Docker Daily Restart" "warning"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ ${#FAILED[@]} -gt 0 ]] && exit 1
exit 0