This commit is contained in:
Mike McFetridge
2026-07-23 19:38:14 -04:00
parent 72272e4006
commit 84e1ead50c
283 changed files with 0 additions and 206 deletions
+150
View File
@@ -0,0 +1,150 @@
# ============================================================
# Tom Spark's ARR Stack — Environment Configuration
# https://github.com/loponai/arrstack
#
# INSTRUCTIONS:
# 1. Copy this file: cp .env.example .env
# 2. Fill in your VPN credentials below
# 3. Adjust timezone and user IDs if needed
# 4. Run: docker compose up -d
# ============================================================
# ============================================================
# SYSTEM SETTINGS
# ============================================================
# Your timezone (list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
TZ=America/New_York
# Your Linux user/group ID. Find yours with: id
# Most systems default to 1000. If yours is different, change these.
PUID=1000
PGID=1000
# ============================================================
# VPN SETTINGS — Pick your provider and fill in credentials
# Full provider list: https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers
#
# IMPORTANT: VPN credentials are NOT your login email/password!
# You need service credentials from your VPN provider's manual
# setup or API section. See the provider examples at the bottom of this file.
# ============================================================
# Your VPN provider (surfshark, nordvpn, protonvpn, airvpn, mullvad, private internet access, windscribe, etc.)
VPN_SERVICE_PROVIDER=airvpn
# Protocol: wireguard (recommended, faster) or openvpn
VPN_TYPE=wireguard
# --- WIREGUARD CREDENTIALS ---
# For Surfshark: go to https://my.surfshark.com/vpn/manual-setup/main → WireGuard
# For other providers: see the examples at the bottom of this file
WIREGUARD_PRIVATE_KEY=eDgf3GPFy2ltgx2RkD/Vx5wKZ4dVi28YbQmSJOrbWmk=
WIREGUARD_ADDRESSES=10.175.23.30
# Only needed for some providers (AirVPN). Leave blank if not required.
# WIREGUARD_PUBLIC_KEY=
WIREGUARD_PRESHARED_KEY=+0+hrSdzRUxKDNk1Q37PNmNJ2jsj2EzF45JtbZad4lI=
# --- OPENVPN CREDENTIALS ---
# Only needed if VPN_TYPE=openvpn. Leave blank if using WireGuard.
# OPENVPN_USER=
# OPENVPN_PASSWORD=
# --- SERVER SELECTION ---
# Pick a country close to you for best speeds
SERVER_COUNTRIES=Canada
# --- PORT FORWARDING ---
# Supported by: ProtonVPN, AirVPN, PIA. Can help with upload speeds and seeding.
# Not required for downloading. Most users don't need this.
# Set to "on" if your provider supports it, leave blank otherwise (Surfshark, NordVPN, etc.).
#VPN_PORT_FORWARDING=
# If your provider requires manually specifying a port (e.g. AirVPN):
FIREWALL_VPN_INPUT_PORTS=29261
# ============================================================
# NETWORK — Static IPs for each service
# You shouldn't need to change these unless you have a conflict.
# ============================================================
IP_GLUETUN=172.39.0.2
IP_RADARR=172.39.0.3
IP_SONARR=172.39.0.4
IP_LIDARR=172.39.0.5
IP_BAZARR=172.39.0.6
IP_JELLYFIN=172.39.0.7
IP_SEERR=172.39.0.8
IP_AUDIO=172.39.0.9
IP_NAVI=172.39.0.10
IP_LIST=172.39.0.11
IP_WATCH=172.39.0.12
# ============================================================
# PROVIDER-SPECIFIC EXAMPLES
# Uncomment and fill in the section for your VPN provider.
# ============================================================
# --- NORDVPN ---
# 1. Go to: https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/
# 2. Generate a WireGuard private key (NordLynx)
# 3. Paste the private key below
# VPN_SERVICE_PROVIDER=nordvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_nordvpn_private_key_here
# WIREGUARD_ADDRESSES=10.5.0.2/16
# SERVER_COUNTRIES=United States
# --- PROTONVPN ---
# 1. Go to: https://account.protonvpn.com/ → Downloads → WireGuard configuration
# 2. Generate a config, open the file, copy the PrivateKey and Address
# 3. Port forwarding is supported on paid plans
# VPN_SERVICE_PROVIDER=protonvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_proton_private_key_here
# WIREGUARD_ADDRESSES=10.2.0.2/32
# SERVER_COUNTRIES=United States
# VPN_PORT_FORWARDING=on
# --- SURFSHARK ---
# 1. Go to: https://my.surfshark.com/vpn/manual-setup/main
# 2. Get WireGuard credentials
# VPN_SERVICE_PROVIDER=surfshark
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_surfshark_private_key_here
# WIREGUARD_ADDRESSES=10.14.0.2/16
# SERVER_COUNTRIES=United States
# --- AIRVPN ---
# 1. Go to: https://airvpn.org/ → Client Area → Config Generator
# 2. Select Linux → WireGuard → pick a server → Generate
# 3. Copy all keys and the assigned IP
# VPN_SERVICE_PROVIDER=airvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_airvpn_private_key_here
# WIREGUARD_PUBLIC_KEY=your_airvpn_public_key_here
# WIREGUARD_PRESHARED_KEY=your_airvpn_preshared_key_here
# WIREGUARD_ADDRESSES=your_assigned_ip/32
# FIREWALL_VPN_INPUT_PORTS=your_forwarded_port
# VPN_PORT_FORWARDING=on
# --- MULLVAD ---
# 1. Go to: https://mullvad.net/en/account → WireGuard configuration
# VPN_SERVICE_PROVIDER=mullvad
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_mullvad_private_key_here
# WIREGUARD_ADDRESSES=your_assigned_ip/32
# SERVER_COUNTRIES=United States
# --- Watchtower ---
WATCHTOWER_CLEANUP=true
WATCHTOWER_INCLUDE_RESTARTING=true
WATCHTOWER_ROLLING_RESTARTING=true
WATCHTOWER_SCHEDULE=0 0 4 * * *
WATCHTOWER_INCLUDE_STOPPED=true
WATCHTOWER_NOTIFICATIONS=email
WATCHTOWER_NOTIFICATIONS_HOSTNAME=Docker-Deply # Add Server Name or IP Address
WATCHTOWER_NOTIFICATION_EMAIL_FROM=miker@mmcfetridge.net
WATCHTOWER_NOTIFICATION_EMAIL_TO=miker@mmcfetridge.net
WATCHTOWER_NOTIFICATION_EMAIL_SERVER=mail.mmcfetridge.net
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=465
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=miker@mmcfetridge.net
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=!Sucyetat123
WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2
PUID=1000
PGID=1000
+440
View File
@@ -0,0 +1,440 @@
# Tom Spark's ARR Stack — Automated Media Server
# https://github.com/loponai/arrstack
#
# Usage:
# 1. Copy .env.example to .env and fill in your VPN credentials
# 2. Run: bash setup-folders.sh
# 3. Run: docker compose up -d
#
# All VPN-protected services (qBittorrent, Prowlarr, FlareSolverr) run
# through Gluetun. If the VPN drops, traffic stops. Zero leaks.
#
# Radarr, Sonarr, Lidarr, Bazarr, Jellyfin, and Seerr do NOT run through
# the VPN — they need direct network access for speed and local connectivity.
networks:
arrnetwork:
name: arrnetwork
ipam:
config:
- subnet: 172.39.0.0/24
services:
# ============================================================
# GLUETUN — VPN Container (kill switch + tunnel)
# All VPN-protected services route through this container.
# Ports for those services are mapped HERE, not on the services themselves.
# Docs: https://github.com/qdm12/gluetun-wiki
# ============================================================
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
networks:
arrnetwork:
ipv4_address: ${IP_GLUETUN}
ports:
- 8000:8000 # Gluetun Control Server
- 8080:8080 # qBittorrent WebUI
- 6881:6881 # qBittorrent torrenting port
- 6881:6881/udp
- 9696:9696 # Prowlarr
- 8191:8191 # FlareSolverr
volumes:
- gluetun_volume:/gluetun
environment:
- VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
- VPN_TYPE=${VPN_TYPE}
# --- WireGuard credentials (most providers) ---
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
# - WIREGUARD_PUBLIC_KEY=${WIREGUARD_PUBLIC_KEY}
- WIREGUARD_PRESHARED_KEY=${WIREGUARD_PRESHARED_KEY}
# --- OpenVPN credentials (if using OpenVPN instead) ---
# - OPENVPN_USER=${OPENVPN_USER}
# - OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
# --- Server selection ---
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
# --- Port forwarding (ProtonVPN, AirVPN, PIA) ---
# - VPN_PORT_FORWARDING=${VPN_PORT_FORWARDING}
- FIREWALL_VPN_INPUT_PORTS=${FIREWALL_VPN_INPUT_PORTS}
- FIREWALL_OUTBOUND_SUBNETS=192.168.0.0/22
# --- General ---
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
- BLOCK_MALICIOUS=off
- HTTP_CONTROL_SERVER_ADDRESS=:8000
- HTTP_CONTROL_SERVER_LOG=on
- HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE={"auth":"none"}
healthcheck:
test: wget -qO /dev/null http://127.0.0.1:9999 || exit 1
interval: 20s
timeout: 10s
retries: 5
restart: unless-stopped
# ============================================================
# QBITTORRENT — Torrent Client (runs through Gluetun VPN)
# ALL traffic goes through the VPN tunnel. No direct internet.
# ============================================================
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: true
labels:
- deunhealth.restart.on.unhealthy=true
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
- WEBUI_PORT=8080
- TORRENTING_PORT=${FIREWALL_VPN_INPUT_PORTS}
volumes:
- qbittorrent_volume:/config
- /data:/data
healthcheck:
test: wget -q --spider http://localhost:8080 || exit 1
interval: 60s
timeout: 10s
retries: 3
start_period: 20s
restart: unless-stopped
# ============================================================
# DEUNHEALTH — Auto-restarts unhealthy containers
# If qBittorrent loses VPN connection, this restarts it automatically.
# ============================================================
deunhealth:
image: qmcgaw/deunhealth
container_name: deunhealth
network_mode: none
environment:
- LOG_LEVEL=info
- HEALTH_SERVER_ADDRESS=127.0.0.1:9999
- TZ=${TZ}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- deunhealth_volume:/config
restart: always
# ============================================================
# PROWLARR — Indexer Manager (runs through Gluetun VPN)
# Manages torrent/usenet indexers. Syncs to Radarr/Sonarr/Lidarr.
# ============================================================
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: true
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- prowlarr_volume:/config
restart: unless-stopped
# ============================================================
# FLARESOLVERR — Cloudflare Bypass (runs through Gluetun VPN)
# Some indexers use Cloudflare protection. This gets around it.
# ============================================================
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: true
volumes:
- flaresolverr_volume:/config
environment:
- LOG_LEVEL=info
- TZ=${TZ}
restart: unless-stopped
# ============================================================
# RADARR — Movie Manager (NOT behind VPN)
# Searches via Prowlarr, sends downloads to qBittorrent,
# renames and hard-links completed files to media folder.
# ============================================================
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- radarr_volume:/config
- /data:/data
ports:
- 7878:7878
networks:
arrnetwork:
ipv4_address: ${IP_RADARR}
restart: unless-stopped
# ============================================================
# SONARR — TV Show Manager (NOT behind VPN)
# Same pattern as Radarr but for TV series.
# ============================================================
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- sonarr_volume:/config
- /data:/data
ports:
- 8989:8989
networks:
arrnetwork:
ipv4_address: ${IP_SONARR}
restart: unless-stopped
# ============================================================
# LIDARR — Music Manager (NOT behind VPN)
# Optional. Comment out if you don't need music automation.
# ============================================================
lidarr:
image: lscr.io/linuxserver/lidarr:latest
container_name: lidarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- lidarr_volume:/config
- /data:/data
ports:
- 8686:8686
networks:
arrnetwork:
ipv4_address: ${IP_LIDARR}
restart: unless-stopped
# ============================================================
# BAZARR — Subtitle Manager (NOT behind VPN)
# Automatically downloads subtitles for movies and TV shows.
# ============================================================
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- bazarr_volume:/config
- /data:/data
ports:
- 6767:6767
networks:
arrnetwork:
ipv4_address: ${IP_BAZARR}
restart: unless-stopped
# ============================================================
# JELLYFIN — Media Server (NOT behind VPN)
# Your personal streaming service. Plays movies, TV, music.
# Needs full bandwidth — never put this behind the VPN.
# ============================================================
jellyfin:
image: lscr.io/linuxserver/jellyfin:latest
container_name: jellyfin
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- jellyfin_volume:/config
- /data/media:/data/media
ports:
- 8096:8096
# Uncomment the lines below to enable hardware transcoding (Intel Quick Sync / VAAPI).
# Only works if your system has Intel/AMD integrated graphics (/dev/dri must exist).
# If you get an error about /dev/dri not found, leave these commented out.
# devices:
# - /dev/dri:/dev/dri
networks:
arrnetwork:
ipv4_address: ${IP_JELLYFIN}
restart: unless-stopped
# ============================================================
# SEERR — Request System (NOT behind VPN)
# Netflix-like UI for requesting movies and TV shows.
# Share this with family — they never need to touch Radarr.
#
# Seerr is the unified successor to Overseerr and Jellyseerr
# (merged under seerr-team). Supports Plex, Jellyfin, and Emby.
#
# Config uses a NAMED Docker volume (not a bind mount). This is
# required: Seerr runs as the `node` user (UID 1000) and a
# bind-mounted host folder is created root-owned, causing a
# permission-denied crash loop. On Windows/WSL, bind mounts also
# corrupt the SQLite DB over SMB. Named volumes fix both cases
# (matches upstream Seerr docs).
#
# Migrating from ./jellyseerr or ./seerr bind mount? See README
# troubleshooting "Migrating Seerr config to a named volume".
# ============================================================
seerr:
image: ghcr.io/seerr-team/seerr:v3.0.1
init: true
container_name: seerr
environment:
- LOG_LEVEL=info
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
- PORT=5055
volumes:
- seerr_volume:/app/config # <--- Needed or will cause a permissions issue.
ports:
- 5055:5055
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/settings/public || exit 1
start_period: 20s
timeout: 3s
interval: 15s
retries: 3
networks:
arrnetwork:
ipv4_address: ${IP_SEERR}
restart: unless-stopped
listenarr:
image: ghcr.io/listenarrs/listenarr:canary
container_name: listenarr
ports:
- "4545:4545"
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- listenarr_volume:/app/config
- /data/media/audiobookshelf/books:/audiobooks
- /data/torrents/books:/downloads
restart: unless-stopped
networks:
arrnetwork:
ipv4_address: ${IP_LIST}
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
ports:
- 13378:80
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- /data/media/books/audiobookshelf/books:/audiobooks
- /data/media/books/audiobookshelf/podcasts:/podcasts
- /data/media/books/audiobookshelf/metadata:/metadata
- audiobookshelf_volume:/config
restart: unless-stopped
networks:
arrnetwork:
ipv4_address: ${IP_AUDIO}
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
ports:
- 4533:4533
restart: unless-stopped
environment:
- ND_SCANSCHEDULE=1h
- ND_LOGLEVEL=info
- ND_SESSIONTIMEOUT=24h
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- navidrome_volume:/data
- /data/media/music:/music:ro
networks:
arrnetwork:
ipv4_address: ${IP_NAVI}
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
env_file: .env
environment:
- DOCKER_API_VERSION=1.44
- TZ=${TZ}
- PUID:=${PUID} # Reference PUID from .env
- PGID=${PGID} # Reference PGID from .env
- WATCHTOWER_CLEANUP=${WATCHTOWER_CLEANUP}
- WATCHTOWER_INCLUDE_RESTARTING=${WATCHTOWER_INCLUDE_RESTARTING}
- WATCHTOWER_ROLLING_RESTARTING=${WATCHTOWER_ROLLING_RESTARTING}
- WATCHTOWER_SCHEDULE=${WATCHTOWER_SCHEDULE}
- WATCHTOWER_INCLUDE_STOPPED=${WATCHTOWER_INCLUDE_STOPPED}
- WATCHTOWER_NOTIFICATIONS=${WATCHTOWER_NOTIFICATIONS}
- WATCHTOWER_NOTIFICATIONS_HOSTNAME=${WATCHTOWER_NOTIFICATIONS_HOSTNAME}
- WATCHTOWER_NOTIFICATION_EMAIL_FROM=${WATCHTOWER_NOTIFICATION_EMAIL_FROM}
- WATCHTOWER_NOTIFICATION_EMAIL_TO=${WATCHTOWER_NOTIFICATION_EMAIL_TO}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD}
security_opt:
- no-new-privileges:true
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock
- watchtower_volume:/config
networks:
arrnetwork:
ipv4_address: ${IP_WATCH}
# Go to the VERY BOTTOM of your file (outside of services) and add this:
volumes:
gluetun_volume:
qbittorrent_volume:
deunhealth_volume:
prowlarr_volume:
flaresolverr_volume:
radarr_volume:
sonarr_volume:
lidarr_volume:
bazarr_volume:
jellyfin_volume:
seerr_volume:
listenarr_volume:
audiobookshelf_volume:
navidrome_volume:
watchtower_volume:
@@ -0,0 +1,44 @@
GNU nano 8.3 setup-folders.bat
@echo off
REM ============================================================
REM Tom Spark's ARR Stack — Folder Structure Setup (Windows)
REM https://github.com/loponai/arrstack
REM
REM Creates the folder structure required for hard links.
REM Run this ONCE before starting the stack.
REM
REM Edit DATA_DIR below if your media drive is different.
REM ============================================================
set DATA_DIR=D:\data
echo.
echo === Tom Spark's ARR Stack — Folder Setup (Windows) ===
echo.
echo Creating folder structure at %DATA_DIR%...
echo.
mkdir "%DATA_DIR%\torrents\movies" 2>nul
mkdir "%DATA_DIR%\torrents\tv" 2>nul
mkdir "%DATA_DIR%\torrents\music" 2>nul
mkdir "%DATA_DIR%\media\movies" 2>nul
mkdir "%DATA_DIR%\media\tv" 2>nul
mkdir "%DATA_DIR%\media\music" 2>nul
echo Done! Folder structure:
echo.
echo %DATA_DIR%\
echo +-- torrents\
echo ¦ +-- movies\
echo ¦ +-- tv\
echo ¦ +-- music\
echo +-- media\
echo +-- movies\
echo +-- tv\
echo +-- music\
echo.
echo IMPORTANT: For hard links to work, torrents and media
echo must be on the SAME drive (both under %DATA_DIR%).
echo.
pause
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# ============================================================
# Tom Spark's ARR Stack — Folder Structure Setup
# https://github.com/loponai/arrstack
#
# Creates the /data directory structure required for hard links
# to work correctly. Run this ONCE before starting the stack.
#
# Usage: sudo bash setup-folders.sh
# ============================================================
set -e
DATA_DIR="/data"
echo ""
echo "=== Tom Spark's ARR Stack — Folder Setup ==="
echo ""
echo "This will create the following structure:"
echo ""
echo " /data/"
echo " ├── torrents/"
echo " │ ├── movies/"
echo " │ ├── tv/"
echo " │ └── music/"
echo " └── media/"
echo " ├── movies/"
echo " ├── tv/"
echo " └── music/"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "ERROR: This script needs sudo to create /data and set permissions."
echo "Run: sudo bash setup-folders.sh"
exit 1
fi
# Get the real user (not root) for ownership
REAL_USER=${SUDO_USER:-$USER}
REAL_UID=$(id -u "$REAL_USER")
REAL_GID=$(id -g "$REAL_USER")
echo "Creating folders..."
mkdir -p "$DATA_DIR"/{torrents/{movies,tv,music,books},media/{movies,tv,music,books}}
echo "Setting ownership to $REAL_USER ($REAL_UID:$REAL_GID)..."
chown -R "$REAL_UID":"$REAL_GID" "$DATA_DIR"
echo "Setting permissions..."
chmod -R 775 "$DATA_DIR"
# Comment this out if you are not deploying this through Gitea CI/CD process.
echo "Setting up directory nd ownership"
# Create the directory if it doesn't exist yet
sudo mkdir -p /opt/Docker-Deployments
# Change the ownership to your deployment user
sudo chown -R miker:miker /opt/Docker-Deployments
echo ""
echo "Done! Folder structure:"
if command -v tree &> /dev/null; then
tree "$DATA_DIR"
else
find "$DATA_DIR" -type d | head -20
fi
echo ""
echo "Your PUID=$REAL_UID and PGID=$REAL_GID"
echo "Make sure these match your .env file."
echo ""
+329
View File
@@ -0,0 +1,329 @@
#!/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