install the "User Scripts" plugin change the VPN_CONTAINER variable to match the name of yours change the CONTAINERS variable to match the name(s) of the containers (separated by a space) connected to GLUETUN_VPN. this script checks if your containers can reach google.com cloudflare.com or amazon.com if they cannot, they get restarted. it also by default sends you an Unraid notification #!/bin/bash
# VPN Container Watchdog Script (Multi-Site Check with Discord Notifications)
# Designed for Unraid to monitor containers running through a VPN (e.g., Gluetun)
# -----------------------------------------------------------------------------
# CONFIGURATION
# -----------------------------------------------------------------------------
# The name of your VPN provider container
VPN_CONTAINER="gluetun-vpn"
# List of dependent containers to check (separated by spaces)
CONTAINERS="prowlarr sonarr radarr"
# List of sites to check for connectivity (Google, Cloudflare, Amazon/AWS)
TEST_URLS=("https://google.com" "https://cloudflare.com" "https://amazon.com")
# How long to wait for recovery in total (900s = 15m)
MAX_WAIT_SECONDS=900
# How often to check for recovery status
POLL_INTERVAL_SECONDS=30
# Path for the lock file to prevent concurrent runs
LOCKFILE="/tmp/vpn_watchdog.lock"
# -----------------------------------------------------------------------------
# FUNCTIONS
# -----------------------------------------------------------------------------
# Send to Unraid's notification system (which forwards to Discord)
send_notification() {
local SUBJECT="$1"
local MESSAGE="$2"
local IMPORTANCE="${3:-normal}"
# Log to the User Scripts log
echo "$(date): [$IMPORTANCE] $SUBJECT: $MESSAGE"
if [ -f /usr/local/emhttp/webGui/scripts/notify ]; then
/usr/local/emhttp/webGui/scripts/notify -e "VPN Containers Watchdog" -s "$SUBJECT" -d "$MESSAGE" -i "$IMPORTANCE"
fi
}
# Check if a specific container is running
is_container_running() {
[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" == "true" ]
}
# Test internet connectivity via curl inside a specified container
check_connectivity() {
local CONTAINER="$1"
# If container isn't running, it's definitely not connected
if ! is_container_running "$CONTAINER"; then
return 1
fi
for URL in "${TEST_URLS[@]}"; do
if docker exec "$CONTAINER" curl -sf --head "$URL" --connect-timeout 5 > /dev/null; then
return 0 # Success
fi
done
return 1 # Failure
}
# Poll until all specified containers are running AND have internet connectivity
check_container_up() {
local TARGET_CONTAINERS=("$@")
local WAIT_TIME=0
echo "$(date): [INFO] Polling for connectivity: ${TARGET_CONTAINERS[*]} (Max ${MAX_WAIT_SECONDS}s)"
while [ $WAIT_TIME -lt $MAX_WAIT_SECONDS ]; do
local STILL_OFFLINE=0
for CONTAINER in "${TARGET_CONTAINERS[@]}"; do
if ! check_connectivity "$CONTAINER"; then
STILL_OFFLINE=$((STILL_OFFLINE + 1))
fi
done
if [ $STILL_OFFLINE -eq 0 ]; then
echo "$(date): [INFO] All polled containers are ONLINE and RUNNING."
return 0
fi
echo "$(date): [INFO] Still waiting for recovery ($STILL_OFFLINE containers remaining)... ($WAIT_TIME/${MAX_WAIT_SECONDS}s)"
sleep $POLL_INTERVAL_SECONDS
WAIT_TIME=$((WAIT_TIME + POLL_INTERVAL_SECONDS))
done
return 1
}
# Robust restart/rebuild for Unraid
restart_container() {
local NAME="$1"
local STATUS=$(docker inspect -f '{{.State.Status}}' "$NAME" 2>/dev/null)
# If it's NOT running, use Unraid's native rebuild
# This recreates the container from XML, fixing stale --network=container:xxx IDs
echo "$(date): [INFO] Performing Unraid XML-based rebuild for $NAME..."
if [ -f /usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container ]; then
/usr/bin/php /usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container "$NAME"
else
echo "$(date): [ERROR] Unraid rebuild script not found! Attempting standard start."
docker start "$NAME"
fi
}
# -----------------------------------------------------------------------------
# MAIN EXECUTION
# -----------------------------------------------------------------------------
# 0. PREVENT CONCURRENT RUNS
if [ -e "$LOCKFILE" ]; then
echo "$(date): [INFO] Another instance is running or a stale lock exists. Exiting."
echo "$(date): [INFO] To manually remove the lock: rm $LOCKFILE"
exit 0
fi
# Create lock and ensure cleanup on exit
touch "$LOCKFILE"
trap 'echo "$(date): --- End VPN Watchdog ---"; rm -f "$LOCKFILE"' EXIT
echo "$(date): --- Starting VPN Watchdog ---"
echo "$(date): [INFO] Starting VPN Containers Watchdog check..."
# 0. VALIDATION: Ensure configured containers actually exist
VALID_CONTAINERS=()
for CONTAINER in $CONTAINERS; do
if docker inspect "$CONTAINER" >/dev/null 2>&1; then
VALID_CONTAINERS+=("$CONTAINER")
else
echo "$(date): [WARN] Container '$CONTAINER' not found. Removing from check list."
send_notification "WARNING: Invalid Container Configuration" "The container '$CONTAINER' was not found on this system and will be skipped." "warning"
fi
done
CONTAINERS="${VALID_CONTAINERS[*]}"
# If no valid containers left, exit early
if [ -z "$CONTAINERS" ]; then
echo "$(date): [ERROR] No valid containers to monitor. Exiting."
exit 1
fi
# 1. PRE-CHECK: Is VPN container itself running?
echo "$(date): [INFO] Checking if $VPN_CONTAINER is running..."
if ! is_container_running "$VPN_CONTAINER"; then
echo "$(date): [WARN] $VPN_CONTAINER is NOT running. Polling for up to ${MAX_WAIT_SECONDS}s..."
WAIT_TIME=0
while ! is_container_running "$VPN_CONTAINER" && [ $WAIT_TIME -lt $MAX_WAIT_SECONDS ]; do
sleep $POLL_INTERVAL_SECONDS
WAIT_TIME=$((WAIT_TIME + POLL_INTERVAL_SECONDS))
echo "$(date): [INFO] Still waiting for $VPN_CONTAINER... ($WAIT_TIME/${MAX_WAIT_SECONDS}s)"
done
if ! is_container_running "$VPN_CONTAINER"; then
send_notification "CRITICAL: VPN Container Down" "$VPN_CONTAINER is STILL not running after ${MAX_WAIT_SECONDS}s. Watchdog is aborting." "alert"
exit 1
fi
send_notification "INFO: VPN Recovered" "$VPN_CONTAINER has started. Proceeding with checks." "normal"
else
echo "$(date): [INFO] $VPN_CONTAINER is running. Proceeding..."
fi
# 2. MAIN CHECK: Identify offline or stopped containers
OFFLINE_CONTAINERS=()
for CONTAINER in $CONTAINERS; do
echo "$(date): [INFO] Checking status and connectivity for: $CONTAINER"
# Check if container is running
STATUS=$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null)
if [ "$STATUS" == "running" ]; then
if ! check_connectivity "$CONTAINER"; then
echo "$(date): [WARN] $CONTAINER is Running but has no internet."
OFFLINE_CONTAINERS+=("$CONTAINER")
else
echo "$(date): [INFO] $CONTAINER is ONLINE."
fi
elif [ -n "$STATUS" ]; then
echo "$(date): [WARN] $CONTAINER is in status '$STATUS'. Adding to recovery list."
OFFLINE_CONTAINERS+=("$CONTAINER")
else
echo "$(date): [INFO] $CONTAINER does not exist. Skipping."
fi
done
# 3. ACTION: If containers are offline: Notify, then Restart/Rebuild
if [ ${#OFFLINE_CONTAINERS[@]} -gt 0 ]; then
# Send the notification first
send_notification "WARNING: VPN Containers Offline" "Issues detected in: ${OFFLINE_CONTAINERS[*]}. Attempting recovery." "warning"
# Perform the recovery actions
for CONTAINER in "${OFFLINE_CONTAINERS[@]}"; do
restart_container "$CONTAINER"
done
# 4. RECOVERY VERIFICATION: Use the refined polling function
check_container_up "${OFFLINE_CONTAINERS[@]}"
# Final status check for notification summary
SUCCESSFUL=()
FAILED=()
for CONTAINER in "${OFFLINE_CONTAINERS[@]}"; do
if check_connectivity "$CONTAINER"; then
SUCCESSFUL+=("$CONTAINER")
else
FAILED+=("$CONTAINER")
fi
done
# Prepare and send summary notification
if [ ${#FAILED[@]} -eq 0 ]; then
send_notification "SUCCESS: VPN Containers Recovered" "The following containers are back online: ${SUCCESSFUL[*]}" "normal"
elif [ ${#SUCCESSFUL[@]} -eq 0 ]; then
send_notification "CRITICAL: VPN Recovery Failed" "The following containers are STILL OFFLINE: ${FAILED[*]}" "alert"
else
send_notification "WARNING: Partial VPN Recovery" "Recovered: ${SUCCESSFUL[*]}. STILL OFFLINE: ${FAILED[*]}" "warning"
fi
fi
echo "$(date): [INFO] VPN Containers Watchdog check complete."