Files
Compose-Files/Docker-Compose/ArrsStack/test-stack.sh
T
2026-07-23 19:38:14 -04:00

329 lines
13 KiB
Bash

#!/bin/bash
# ============================================================
# Tom Spark's ARR Stack — Health Check & Troubleshooting
# https://github.com/loponai/arrstack
#
# Run this after 'docker compose up -d' to verify everything
# is working correctly. It checks each service, tests VPN
# connectivity, and provides specific fixes for any issues.
#
# Usage: bash test-stack.sh
# ============================================================
set -o pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
PASS="${GREEN}✓ PASS${NC}"
FAIL="${RED}✗ FAIL${NC}"
WARN="${YELLOW}! WARN${NC}"
TOTAL_PASS=0
TOTAL_FAIL=0
TOTAL_WARN=0
pass() { echo -e " ${PASS} $1"; ((TOTAL_PASS++)); }
fail() { echo -e " ${FAIL} $1"; ((TOTAL_FAIL++)); }
warn() { echo -e " ${WARN} $1"; ((TOTAL_WARN++)); }
header() { echo -e "\n${CYAN}${BOLD}[$1]${NC}"; }
fix() { echo -e " ${YELLOW}Fix: $1${NC}"; }
echo ""
echo "========================================="
echo " Tom Spark's ARR Stack — Health Check"
echo "========================================="
echo ""
# ============================================================
# TEST 1: Docker running?
# ============================================================
header "Docker"
if docker info > /dev/null 2>&1; then
pass "Docker is running"
else
fail "Docker is not running"
fix "Start Docker: sudo systemctl start docker"
fix "Or install: curl -fsSL https://get.docker.com | sh"
echo ""
echo "Cannot continue without Docker. Exiting."
exit 1
fi
# ============================================================
# TEST 2: .env file exists and has VPN credentials?
# ============================================================
header "Configuration"
if [ -f .env ]; then
pass ".env file exists"
else
fail ".env file not found"
fix "Run: cp .env.example .env && nano .env"
fix "Then fill in your VPN credentials"
fi
if [ -f .env ]; then
VPN_KEY=$(grep -E "^WIREGUARD_PRIVATE_KEY=" .env 2>/dev/null | cut -d= -f2)
VPN_PROVIDER=$(grep -E "^VPN_SERVICE_PROVIDER=" .env 2>/dev/null | cut -d= -f2)
if [ -n "$VPN_KEY" ] && [ "$VPN_KEY" != "" ]; then
pass "VPN private key is set (provider: $VPN_PROVIDER)"
else
fail "VPN private key is empty"
fix "Edit .env and paste your WireGuard private key"
fix "Get it from your VPN provider's manual setup page"
fi
fi
# ============================================================
# TEST 3: Folder structure exists?
# ============================================================
header "Folder Structure"
ALL_FOLDERS_OK=true
for dir in /data/torrents/movies /data/torrents/tv /data/torrents/music /data/torrents/books /data/media/movies /data/media/tv /data/media/music /data/media/books; do
if [ -d "$dir" ]; then
pass "$dir exists"
else
fail "$dir missing"
ALL_FOLDERS_OK=false
fi
done
if [ "$ALL_FOLDERS_OK" = false ]; then
fix "Run: sudo bash setup-folders.sh"
fi
# Check permissions
if [ -d /data ]; then
OWNER=$(stat -c '%u' /data 2>/dev/null)
ENV_PUID=$(grep -E "^PUID=" .env 2>/dev/null | cut -d= -f2)
if [ "$OWNER" = "$ENV_PUID" ] || [ "$OWNER" = "$(id -u)" ]; then
pass "/data ownership matches PUID ($OWNER)"
else
warn "/data owned by $OWNER but PUID is ${ENV_PUID:-1000}"
fix "Run: sudo chown -R ${ENV_PUID:-1000}:${ENV_PUID:-1000} /data"
fi
fi
# ============================================================
# TEST 4: Container status
# ============================================================
header "Containers"
EXPECTED_SERVICES="gluetun qbittorrent deunhealth prowlarr flaresolverr radarr sonarr lidarr bazarr jellyfin seerr"
for svc in $EXPECTED_SERVICES; do
STATUS=$(docker inspect --format '{{.State.Status}}' "$svc" 2>/dev/null)
HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$svc" 2>/dev/null)
if [ -z "$STATUS" ]; then
fail "$svc — not found (not created)"
fix "Run: docker compose up -d"
elif [ "$STATUS" = "running" ]; then
if [ "$HEALTH" = "healthy" ]; then
pass "$svc — running (healthy)"
elif [ "$HEALTH" = "unhealthy" ]; then
fail "$svc — running but UNHEALTHY"
if [ "$svc" = "gluetun" ]; then
fix "VPN probably can't connect. Check credentials in .env"
fix "Check logs: docker logs gluetun | tail -20"
fix "Try: rm -rf gluetun && docker compose up -d gluetun"
elif [ "$svc" = "qbittorrent" ]; then
fix "Usually means VPN dropped. Deunhealth should auto-restart it."
fix "Check: docker logs qbittorrent | tail -20"
fi
elif [ "$HEALTH" = "starting" ]; then
warn "$svc — running (health check starting, wait 30s and rerun)"
else
pass "$svc — running"
fi
elif [ "$STATUS" = "created" ]; then
warn "$svc — created but not started"
if [ "$svc" = "qbittorrent" ] || [ "$svc" = "prowlarr" ] || [ "$svc" = "flaresolverr" ]; then
fix "Waiting for Gluetun to be healthy. Check Gluetun status first."
fix "If Gluetun is healthy, try: docker compose up -d $svc"
elif [ "$svc" = "seerr" ]; then
fix "Port 5055 may be in use. Check: ss -tlnp | grep 5055"
fix "Or change the port in docker-compose.yml"
else
fix "Try: docker compose up -d $svc"
fi
elif [ "$STATUS" = "restarting" ]; then
fail "$svc — crash-looping (restarting)"
fix "Check logs: docker logs $svc | tail -30"
if [ "$svc" = "seerr" ]; then
fix "Seerr may have a corrupt config. Try: docker compose down seerr && rm -rf seerr && docker compose up -d seerr"
fix "WSL/Windows users: if it keeps crashing, try a named volume instead of a bind mount"
else
fix "Try: docker compose down $svc && docker compose up -d $svc"
fi
elif [ "$STATUS" = "exited" ]; then
fail "$svc — exited (crashed)"
fix "Check logs: docker logs $svc | tail -30"
fix "Try restarting: docker compose up -d $svc"
else
warn "$svc — status: $STATUS"
fi
done
# ============================================================
# TEST 5: VPN connectivity
# ============================================================
header "VPN Connection"
GLUETUN_STATUS=$(docker inspect --format '{{.State.Status}}' gluetun 2>/dev/null)
GLUETUN_HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{end}}' gluetun 2>/dev/null)
if [ "$GLUETUN_STATUS" = "running" ] && [ "$GLUETUN_HEALTH" = "healthy" ]; then
# Get VPN IP
VPN_IP=$(docker exec gluetun wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ -n "$VPN_IP" ]; then
pass "Gluetun VPN IP: $VPN_IP"
# Get VPN location
VPN_LOCATION=$(docker exec gluetun wget -qO- --timeout=10 "ipinfo.io/${VPN_IP}/city" 2>/dev/null)
VPN_COUNTRY=$(docker exec gluetun wget -qO- --timeout=10 "ipinfo.io/${VPN_IP}/country" 2>/dev/null)
if [ -n "$VPN_LOCATION" ]; then
pass "VPN location: $VPN_LOCATION, $VPN_COUNTRY"
fi
else
fail "Gluetun is healthy but can't reach the internet"
fix "Check logs: docker logs gluetun | tail -20"
fi
# Check if qBittorrent is tunneled
QBIT_STATUS=$(docker inspect --format '{{.State.Status}}' qbittorrent 2>/dev/null)
if [ "$QBIT_STATUS" = "running" ]; then
QBIT_IP=$(docker exec qbittorrent wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ "$QBIT_IP" = "$VPN_IP" ]; then
pass "qBittorrent tunneled through VPN ($QBIT_IP)"
elif [ -n "$QBIT_IP" ]; then
fail "qBittorrent IP ($QBIT_IP) doesn't match VPN IP ($VPN_IP)!"
fix "This should not happen. Check network_mode in docker-compose.yml"
else
warn "Could not check qBittorrent IP (container may still be starting)"
fi
fi
# Check if Prowlarr is tunneled
PROWLARR_STATUS=$(docker inspect --format '{{.State.Status}}' prowlarr 2>/dev/null)
if [ "$PROWLARR_STATUS" = "running" ]; then
PROWLARR_IP=$(docker exec prowlarr wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ "$PROWLARR_IP" = "$VPN_IP" ]; then
pass "Prowlarr tunneled through VPN ($PROWLARR_IP)"
elif [ -n "$PROWLARR_IP" ]; then
fail "Prowlarr IP ($PROWLARR_IP) doesn't match VPN IP ($VPN_IP)!"
fi
fi
# Verify your real IP is different
REAL_IP=$(wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ -n "$REAL_IP" ] && [ "$REAL_IP" != "$VPN_IP" ]; then
pass "Real IP ($REAL_IP) differs from VPN IP — VPN is working!"
elif [ "$REAL_IP" = "$VPN_IP" ]; then
warn "Real IP matches VPN IP — are you already running a system-wide VPN?"
fi
else
if [ "$GLUETUN_HEALTH" = "unhealthy" ]; then
fail "Gluetun is unhealthy — VPN not connected"
fix "Check credentials in .env (these are NOT your VPN login email/password)"
fix "Check logs: docker logs gluetun 2>&1 | tail -30"
fix "Try resetting: docker compose down && rm -rf gluetun && docker compose up -d"
elif [ "$GLUETUN_HEALTH" = "starting" ]; then
warn "Gluetun health check still starting — wait 30-60 seconds and rerun"
else
warn "Gluetun not running — can't test VPN"
fix "Run: docker compose up -d"
fi
fi
# ============================================================
# TEST 6: Service web UI accessibility
# ============================================================
header "Web UI Access"
check_http() {
local name=$1 port=$2
local code=$(curl -sL -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:$port" 2>/dev/null)
if [ "$code" = "200" ] || [ "$code" = "302" ] || [ "$code" = "301" ] || [ "$code" = "307" ]; then
pass "$name — http://localhost:$port (HTTP $code)"
elif [ "$code" = "000" ]; then
# Container might be behind gluetun, check if it's running
local status=$(docker inspect --format '{{.State.Status}}' "$name" 2>/dev/null)
if [ "$status" = "running" ]; then
warn "$name — container running but port $port not reachable from host"
fix "Port may be mapped on Gluetun. Try: http://localhost:$port"
else
fail "$name — not reachable (container not running)"
fi
else
warn "$name — http://localhost:$port returned HTTP $code"
fi
}
check_http qbittorrent 8080
check_http prowlarr 9696
check_http radarr 7878
check_http sonarr 8989
check_http lidarr 8686
check_http bazarr 6767
check_http jellyfin 8096
check_http seerr 5055
# ============================================================
# TEST 7: Hard link capability
# ============================================================
header "Hard Links"
if [ -d /data/torrents ] && [ -d /data/media ]; then
# Check if same filesystem
FS_TORRENTS=$(df /data/torrents --output=source 2>/dev/null | tail -1)
FS_MEDIA=$(df /data/media --output=source 2>/dev/null | tail -1)
if [ "$FS_TORRENTS" = "$FS_MEDIA" ]; then
pass "torrents/ and media/ are on the same filesystem ($FS_TORRENTS)"
pass "Hard links will work correctly"
else
fail "torrents/ ($FS_TORRENTS) and media/ ($FS_MEDIA) are on DIFFERENT filesystems!"
fix "Hard links only work on the same filesystem/drive"
fix "Move both directories to the same drive"
fi
# Quick hard link test
TEST_FILE="/data/torrents/.hardlink_test_$$"
TEST_LINK="/data/media/.hardlink_test_$$"
if touch "$TEST_FILE" 2>/dev/null && ln "$TEST_FILE" "$TEST_LINK" 2>/dev/null; then
pass "Hard link test succeeded"
rm -f "$TEST_FILE" "$TEST_LINK" 2>/dev/null
elif [ -f "$TEST_FILE" ]; then
fail "Hard link test failed — filesystem may not support hard links"
fix "Check filesystem type: df -T /data"
fix "Hard links work on ext4, btrfs, xfs. NOT on exFAT or ntfs-3g"
rm -f "$TEST_FILE" 2>/dev/null
else
warn "Could not write to /data/torrents (permission issue?)"
fix "Run: sudo chown -R $(id -u):$(id -g) /data"
fi
else
warn "Folder structure not found — skipping hard link test"
fix "Run: sudo bash setup-folders.sh"
fi
# ============================================================
# SUMMARY
# ============================================================
echo ""
echo "========================================="
echo -e " ${GREEN}Passed: $TOTAL_PASS${NC} ${RED}Failed: $TOTAL_FAIL${NC} ${YELLOW}Warnings: $TOTAL_WARN${NC}"
echo "========================================="
if [ $TOTAL_FAIL -eq 0 ] && [ $TOTAL_WARN -eq 0 ]; then
echo -e "\n ${GREEN}${BOLD}All checks passed! Your stack is ready to go.${NC}\n"
elif [ $TOTAL_FAIL -eq 0 ]; then
echo -e "\n ${YELLOW}${BOLD}No failures, but check the warnings above.${NC}\n"
else
echo -e "\n ${RED}${BOLD}Some checks failed. Follow the fix instructions above.${NC}"
echo -e " ${BOLD}If stuck, check: docker logs <container-name>${NC}\n"
fi