--- tags: - Bookstack - Documentation - Notes --- # ArrStack Media config documentation # ArrStack Compose file Directory Structure cd docker mkdir -p ~/arr-stack/{config/{gluetun,qbittorrent,prowlarr,radarr,sonarr},data/{torrents,media/{movies,tv}}} cd ~/arr-stack touch docker-compose.yml .env ``` # 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:/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} # --- General --- - 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:/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 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:/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 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:/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:/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:/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:/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:/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 - TZ=${TZ} - PORT=5055 volumes: - seerr_config:/app/config 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 # ============================================================ # AUDIO BookShelf — Server (NOT behind VPN) # Your personal streaming service. Plays all audiobooks. # Needs full bandwidth — never put this behind the VPN. # ============================================================ audiobookshelf: image: ghcr.io/advplyr/audiobookshelf:latest container_name: audiobookshelf ports: - 13378:80 volumes: - /data/media/audiobookshelf/books:/audiobooks - /data/media/audiobookshelf/podcasts:/podcasts - /data/media/audiobookshelf/metadata:/metadata - ./audiobookshelf/config:/config restart: unless-stopped networks: arrnetwork: ipv4_address: ${IP_AUDIO} # ============================================================ # NAVIDROME — Music Media Server (NOT behind VPN) # Your personal streaming service. Plays music. # Needs full bandwidth — never put this behind the VPN. # ============================================================ navidrome: image: deluan/navidrome:latest user: 1000:1000 container_name: navidrome ports: - 4533:4533 restart: unless-stopped environment: ND_SCANSCHEDULE: "1h" ND_LOGLEVEL: info ND_SESSIONTIMEOUT: "24h" volumes: - ./navi/data:/data - /data/media/music:/music:ro networks: arrnetwork: ipv4_address: ${IP_NAVI} volumes: seerr_config: ``` ## 2. Step-by-Step qBittorrent Configuration Once your stack is running via `docker compose up -d`, navigate to your qBittorrent interface at `http://localhost:8118` or your server's IP address (e.g., `[http://192.168.1.50:8118](http://192.168.1.50:8118)`). ### Step 2a: Log In and Find Credentials - Look at your container logs (`docker logs qbittorrent`) to locate the **temporary password** generated automatically by linuxserver/qbittorrent for security. - Use `admin` as the username and paste that password. - _Recommended:_ Instantly go to **Tools > Options > Web UI** to change the password to a permanent one. ### Step 2b: Configure Connection & Port Forwarding This ensures your traffic efficiently routes through AirVPN's system: **1.Open Connection Settings:**Inside Web UI. Go to **Tools** in the top navigation bar and select **Options**. In the sidebar menu that pops up, click on **Connection**. **2.Set the Torrenting Port:**Match Gluetun mapping. Locate **Port used for incoming connections** and type in exactly: `29261`. **3.Disable UPnP / NAT-PMP:**Security step. **Uncheck** the box next to _Use UPnP / NAT-PMP port forwarding from my router_. Your VPN handles port assignment, so your local router shouldn't intervene. ### Step 2c: Bind to the VPN Interface (The Hard Kill Switch) Binding ensures that if Gluetun ever collapses, leaks, or drops connection, qBittorrent immediately stops downloading or seeding instead of trying to pass data via an unprotected network. **1.Open Advanced Panel:**Scroll down sidebar. While still in the Options menu, scroll down the left sidebar panel and select **Advanced**. **2.Bind Network Interface:**Target VPN tunnel. Look for **Network interface** near the top of the list. Change the dropdown menu from _Any interface_ to exactly **`tun0`**. **3.Bind IP Address:**Optional but safer. Look right underneath at the **Optional IP address to bind to** setting. Change it from _All addresses_ to **`All IPv4 addresses`**. **4.Save and Restart:**Apply changes. Click **Save / Apply** at the bottom right. Restart your container with `docker compose restart qbittorrent` to guarantee the network interfaces lock into place cleanly. # .env file ``` # ============================================================ # 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 # ============================================================ # 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 ``` # README.md > ## ⚡ Want the easy, full version? → **[SparkBox](https://tomsparkbox.com)** > > This is one of my original one-shot scripts. It still works — but these days everything I build goes into \*\*\[SparkBox\](https://tomsparkbo> > > ### 👉 Get it free at **[tomsparkbox.com](https://tomsparkbox.com)** > > *Built by [Tom Spark Reviews](https://youtube.com/@TomSparkReviews).* --- # Tom Spark's ARR Stack One-command automated media server with VPN protection. Sonarr, Radarr, Prowlarr, qBittorrent, Gluetun, Jellyfin, and more. **Full video tutorial:** \[YouTube Link Coming Soon\] ## What You Get
ServicePortPurpose
GluetunVPN tunnel with kill switch
qBittorrent8080Torrent client (VPN protected)
Prowlarr9696Indexer manager (VPN protected)
FlareSolverr8191Cloudflare bypass (VPN protected)
Radarr7878Movie automation
Sonarr8989TV show automation
Lidarr8686Music automation
Bazarr6767Subtitle automation
Jellyfin8096Media server / streaming
Seerr5055Netflix-like request UI (Overseerr/Jellyseerr successor)
All download traffic routes through Gluetun's VPN tunnel. If the VPN drops, all traffic stops — zero leaks. The deunhealth container auto-r> ## Quick Start ### 1. Install Docker ```bash curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # Log out and back in for group change to take effect ``` ### 2. Clone this repo ```bash git clone https://github.com/loponai/arrstack.git cd arrstack ``` ### 3. Create folder structure ```bash sudo bash setup-folders.sh ``` This creates: ``` /data/ ├── torrents/ ← qBittorrent downloads here │ ├── movies/ │ ├── tv/ │ └── music/ └── media/ ← Radarr/Sonarr organize files here (Jellyfin reads from here) ├── movies/ ├── tv/ └── music/ ``` > **Hard links:** Both folders MUST be on the same drive/filesystem. Radarr and Sonarr create hard links (not copies) — the file appears in> ### 4. Configure your VPN ```bash cp .env.example .env nano .env ``` Fill in your VPN provider and credentials. See [VPN Setup Guides](#vpn-setup-guides) below. ### 5. Launch ```bash docker compose up -d ``` ### 6. Verify everything is working ```bash bash test-stack.sh ``` This runs a full health check — Docker status, VPN connection, IP leak test, service accessibility, hard link support, and folder permissio> You can also check manually: ```bash # Check Gluetun's IP (should be VPN, not your real IP) docker exec gluetun wget -qO- ifconfig.me # qBittorrent shares Gluetun's network, so the above proves both are tunneled. docker exec qbittorrent wget -qO- ifconfig.me # Check health status of all containers docker ps --format "table {{.Names}}\t{{.Status}}" ``` ### 7. Configure services Open each service in your browser at `http://YOUR-SERVER-IP:PORT` and follow the video tutorial for step-by-step configuration. **Quick reference:** - **qBittorrent** (`:8080`) — Get temp password: `docker logs qbittorrent 2>&1 | grep "temporary password"` - **Prowlarr** (`:9696`) — Add indexers, connect to Radarr/Sonarr. If an indexer is blocked by Cloudflare, set up FlareSolverr as a proxy: > - **Radarr** (`:7878`) — Root folder: `/data/media/movies`, download client category: `movies` - **Sonarr** (`:8989`) — Root folder: `/data/media/tv`, download client category: `tv` - **Jellyfin** (`:8096`) — Add libraries: `/data/media/movies`, `/data/media/tv`, `/data/media/music`. To watch, open `http://YOUR-SERVER-I> - **Seerr** (`:5055`) — Connect to Jellyfin, Radarr, and Sonarr. Seerr is the unified successor to Overseerr/Jellyseerr. If you previously > **Internal Docker IPs — use these when connecting services to each other (NOT localhost):**
IPService
`172.39.0.2`Gluetun (also qBittorrent, Prowlarr, FlareSolverr)
`172.39.0.3`Radarr
`172.39.0.4`Sonarr
`172.39.0.5`Lidarr
`172.39.0.6`Bazarr
`172.39.0.7`Jellyfin
`172.39.0.8`Seerr
These IPs are the same for everyone — they're hardcoded in the docker-compose file. **Common connections:** - Radarr/Sonarr → Download Client → qBittorrent: host `172.39.0.2`, port `8080` - Prowlarr → Apps → Radarr: server `http://172.39.0.3:7878` - Prowlarr → Apps → Sonarr: server `http://172.39.0.4:8989` - Prowlarr → Apps → Prowlarr Server: `http://172.39.0.2:9696` - Seerr → Radarr: host `172.39.0.3`, port `7878` - Seerr → Sonarr: host `172.39.0.4`, port `8989` - Seerr → Jellyfin: host `172.39.0.7`, port `8096` **Important Radarr/Sonarr settings:** - Media Management → Show Advanced → **Use Hardlinks instead of Copy** → must be ON - Media Management → **Rename Movies/Episodes** → recommended ON **Recommended quality profile (1080p baseline, 4K preferred):** Go to Settings → Profiles and edit or create a profile: 1. Uncheck everything below 1080p (720p, 480p, etc.) 2. Check/enable everything from **HDTV-1080p** up through **Bluray-2160p** 3. Set **Cutoff** to `Bluray-1080p` — this is the minimum quality Radarr/Sonarr will be happy with 4. Set **Upgrade Until** to `Bluray-2160p` — it will automatically upgrade to 4K if one becomes available This means it grabs a 1080p release right away so you can start watching, then silently upgrades to 4K later if it finds one. ## VPN Setup Guides ### Surfshark (Recommended) The best value for torrenting — cheapest long-term plans, fast WireGuard speeds, and easy setup with Gluetun. \[Get Surfshark\](https://get.s> 1. Go to [Surfshark Manual Setup](https://my.surfshark.com/vpn/manual-setup/main) 2. Select **WireGuard** and get your credentials (private key + address) 3. In your `.env`: ``` VPN_SERVICE_PROVIDER=surfshark VPN_TYPE=wireguard WIREGUARD_PRIVATE_KEY=your_key_here WIREGUARD_ADDRESSES=10.14.0.2/16 SERVER_COUNTRIES=United States ``` > The `.env.example` file is pre-configured for Surfshark. Just paste your private key and you're good to go. ### NordVPN 1. Go to [NordVPN Manual Setup](https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/) 2. Select **NordLynx** (WireGuard) and generate a private key 3. In your `.env`: ``` VPN_SERVICE_PROVIDER=nordvpn VPN_TYPE=wireguard WIREGUARD_PRIVATE_KEY=your_key_here WIREGUARD_ADDRESSES=10.5.0.2/16 SERVER_COUNTRIES=United States ``` ### ProtonVPN 1. Go to [ProtonVPN WireGuard Config](https://account.protonvpn.com/) → Downloads → WireGuard 2. Generate a config file, open it, copy `PrivateKey` and `Address` 3. In your `.env`: ``` VPN_SERVICE_PROVIDER=protonvpn VPN_TYPE=wireguard WIREGUARD_PRIVATE_KEY=your_key_here WIREGUARD_ADDRESSES=10.2.0.2/32 SERVER_COUNTRIES=United States VPN_PORT_FORWARDING=on ``` ### AirVPN 1. Go to [AirVPN Config Generator](https://airvpn.org/) → Client Area → Config Generator 2. Select Linux → WireGuard → pick server → Generate 3. In your `.env`: ``` VPN_SERVICE_PROVIDER=airvpn VPN_TYPE=wireguard WIREGUARD_PRIVATE_KEY=your_key_here WIREGUARD_PUBLIC_KEY=server_public_key WIREGUARD_PRESHARED_KEY=your_preshared_key WIREGUARD_ADDRESSES=your_ip/32 FIREWALL_VPN_INPUT_PORTS=your_port VPN_PORT_FORWARDING=on ``` ### Other Providers Gluetun supports 30+ providers. Check the [full provider list](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers). ## Starting and Stopping **Start the stack:** ```bash cd arrstack docker compose up -d ``` **Stop the stack:** ```bash cd arrstack docker compose down ``` **Check status:** ```bash docker ps --format "table {{.Names}}\t{{.Status}}" ``` ### Auto-Start After Reboot All containers are set to `restart: unless-stopped`, which means they automatically come back once Docker is running. You just need to make> **Linux (dedicated server or VM):** Run this once and you're done: ```bash sudo systemctl enable docker ``` **Windows (running Docker inside WSL):** WSL (Windows Subsystem for Linux) doesn't start Docker automatically when your PC boots. Here's how to fix that: **Step 1:** Open your WSL terminal and run this command to edit the WSL config file: ```bash sudo nano /etc/wsl.conf ``` **Step 2:** Your file might already have some lines in it (like `[boot]` or `[user]`). Look for a `[boot]` section. If it exists, add the `> ```ini [boot] command=service docker start ``` **Step 3:** Save the file by pressing `Ctrl+X`, then `Y`, then `Enter`. **Step 4 (optional):** By default, WSL only starts when you open a terminal. If you want it to start automatically when Windows boots (so y> 1. Press `Win+R` on your keyboard 2. Type `shell:startup` and press Enter — this opens your Windows Startup folder 3. Right-click in the folder → New → Text Document 4. Name it `wsl.vbs` (make sure it ends in `.vbs`, not `.vbs.txt` — if you can't see file extensions, go to View → Show → File name extensi> 5. Right-click the file → Edit (or Open with Notepad) and paste this: ```vbs Set ws = CreateObject("Wscript.Shell") ws.Run "wsl -d Ubuntu -u root -- service docker start", 0 ``` 6. Save and close That's it — next time your PC restarts, WSL starts Docker automatically and all your containers come back up on their own. No commands need> ## Updating ```bash cd arrstack docker compose pull docker compose up -d ``` ## Remote Access with Tailscale (Optional) Want to access Jellyfin, Seerr, or any service from outside your home? [Tailscale](https://tailscale.com/) creates a private network betwee> **On your server:** ```bash curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up ``` **On your phone/laptop/TV:** 1. Install Tailscale from your app store 2. Sign in with the same account **Access your services from anywhere:** ``` http://YOUR-TAILSCALE-IP:8096 ← Jellyfin http://YOUR-TAILSCALE-IP:5055 ← Seerr http://YOUR-TAILSCALE-IP:7878 ← Radarr http://YOUR-TAILSCALE-IP:8989 ← Sonarr ``` Find your Tailscale IP with `tailscale ip -4` on the server. Tailscale is free for personal use (up to 100 devices). Everything is encrypted with WireGuard — nobody can see your traffic, not even Tail> > **Do NOT expose Jellyfin directly to the internet** (no port forwarding on your router). Use Tailscale or a reverse proxy instead. Direct> ### Sharing with Family and Friends Your family and friends only need two things — **Seerr** to request movies/shows and **Jellyfin** to watch them. They never see Radarr, Son> **Step 1: Invite them to your Tailscale network** 1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/machines) 2. Click **Share** on your server's machine 3. Enter their email — they'll get an invite link **Step 2: They install Tailscale** 1. Download Tailscale on their phone, laptop, or TV from [tailscale.com/download](https://tailscale.com/download) 2. Accept your invite and sign in **Step 3: They access your services** Give them these two links (replace with your Tailscale IP): ``` http://YOUR-TAILSCALE-IP:5055 ← Seerr (request movies and shows) http://YOUR-TAILSCALE-IP:8096 ← Jellyfin (watch everything) ``` **For TVs and phones**, they can install the **Jellyfin app** (available on Roku, Fire TV, Apple TV, Android TV, iOS, Android) and enter yo> That's it — they request, you automatically download, they watch. No technical knowledge needed on their end. ## Troubleshooting **Gluetun unhealthy / won't connect:** - Double-check VPN credentials in `.env` — these are NOT your login email/password - Try removing the gluetun folder and restarting: `rm -rf gluetun && docker compose up -d` - Check logs: `docker logs gluetun` **qBittorrent can't connect:** - Make sure Gluetun is healthy: `docker ps` (should show "healthy") - Check qBit is using VPN: `docker exec gluetun wget -qO- ifconfig.me` - In qBittorrent settings → Advanced → set Network Interface to `tun0` **Movie or show not downloading:** - **Quality profile too strict** — If Radarr/Sonarr can't find a release matching your quality profile, it won't download anything. Go to t> - **Not enough indexers** — Public indexers have limited catalogs. If you only have one or two indexers in Prowlarr, add more (1337x, The P> - **Not enough seeders** — Some torrents just don't have anyone sharing them, especially older or niche content. Check qBittorrent — if the> - **Indexer blocked by Cloudflare** — See the Prowlarr setup note above about setting up FlareSolverr with tags. **Hard links not working (files copying instead):** - Both `/data/torrents` and `/data/media` must be on the same filesystem - Check Radarr/Sonarr → Settings → Media Management → "Use Hardlinks" is checked - Verify with: `ls -i /data/torrents/movies/yourfile` and `ls -i /data/media/movies/YourMovie/yourfile` — inode numbers should match **Permission errors:** - Run `id` and make sure PUID/PGID in `.env` match your user - Re-run: `sudo chown -R $(id -u):$(id -g) /data` **Service won't start — "port already in use":** Another program on your system might be using the same port. This is common with port 5055 (Seerr) but can happen with any service. 1. Find what's using the port (replace `5055` with the port number from the error): **Linux:** ```bash sudo ss -tlnp | grep 5055 ``` **Windows (WSL users) — run in PowerShell:** ```powershell netstat -ano | findstr :5055 ``` This gives you a PID (process ID). Find the program name: ```powershell Get-Process -Id | Select-Object ProcessName, Id, Path ``` 2. Either stop/disable that program, or change the port in `docker-compose.yml` to an unused one (e.g. `5056:5055`). 3. If it's a Windows service hogging the port, disable it in an admin PowerShell: ```powershell Stop-Service -Force Set-Service -StartupType Disabled ``` 4. Then recreate the container: ```bash docker compose up -d --force-recreate ``` **Seerr stuck restarting / crash-looping:** The current `docker-compose.yml` uses a named volume (`seerr_config`) for Seerr's config, which fixes both causes of the crash loop. If you> Root cause (for reference): - **Linux/macOS:** Seerr runs as the `node` user (UID 1000). A bind-mounted `./seerr` folder is created root-owned on first `docker compose> - **Windows/WSL:** Bind mounts go through an SMB share inside Docker Desktop's VM, which doesn't support file locking. Seerr's SQLite DB co> Named volumes sidestep both problems because Docker creates them with correct ownership inside its own managed storage. If you still see it after pulling: - Check logs: `docker logs seerr | tail -30` - Nuke the volume and start fresh (you'll lose Seerr settings, not media): ```bash docker compose down seerr && docker volume rm arrstack_seerr_config && docker compose up -d seerr ``` **Migrating Seerr config to a named volume (existing installs):** If you had Seerr working on an older version of this repo with a bind-mounted `./jellyseerr` or `./seerr` folder and want to keep your sett> ```bash docker compose down seerr # pick whichever folder you actually have SRC=./seerr [ -d ./jellyseerr ] && SRC=./jellyseerr docker volume create arrstack_seerr_config docker run --rm -v "$(pwd)/${SRC#./}":/src -v arrstack_seerr_config:/dest alpine sh -c "cp -a /src/. /dest/ && chown -R 1000:1000 /dest" docker compose up -d seerr ``` If you don't care about preserving settings, just `docker compose up -d seerr` — Seerr will start fresh and walk you through setup again. **Can't log into qBittorrent:** - qBittorrent generates a temporary password every time it starts. Get it with: ```bash docker logs qbittorrent 2>&1 | grep "temporary password" ``` - Default username is `admin`. Once logged in, go to Tools → Options → Web UI and set a permanent password. **Services can't connect to each other (connection refused, timeout):** - Don't use `localhost` when connecting services together — that won't work across Docker containers. - Use the internal Docker IPs instead: - qBittorrent/Prowlarr/FlareSolverr: `172.39.0.2` - Radarr: `172.39.0.3` - Sonarr: `172.39.0.4` - Jellyfin: `172.39.0.7` - The one exception: Prowlarr → FlareSolverr can use `localhost:8191` because they both run through Gluetun and share the same network. **"Root folder does not exist" in Radarr/Sonarr:** - Make sure you ran `sudo bash setup-folders.sh` to create the `/data` directory structure. - Double-check the root folder path — it should be `/data/media/movies` for Radarr and `/data/media/tv` for Sonarr (not `/movies` or `/data> **Downloads stuck at "importing" or "waiting to import":** - This is almost always a permissions issue. Fix it with: ```bash sudo chown -R $(id -u):$(id -g) /data sudo chmod -R 775 /data ``` - Make sure PUID/PGID in your `.env` match your user (check with `id`). **Jellyfin library is empty after downloads finish:** - Make sure your Jellyfin libraries point to the correct paths: `/data/media/movies`, `/data/media/tv`, `/data/media/music` - Jellyfin doesn't scan instantly. Go to Dashboard → Libraries → click the `...` menu → **Scan Library** to force a refresh. - You can also set up scheduled scans in Dashboard → Scheduled Tasks. **Subtitles not downloading (Bazarr):** - Bazarr needs to be connected to Radarr and Sonarr: Settings → Radarr / Sonarr → enter the IP (`172.39.0.3` / `172.39.0.4`) and API key. - You also need at least one subtitle provider: Settings → Providers → Add → **OpenSubtitles.com** is the most popular (free account requir> **Everything works but downloads are slow:** - Your VPN server might be too far away. Change `SERVER_COUNTRIES` in your `.env` to a country closer to you, then restart: ```bash docker compose down && docker compose up -d ``` - Check your VPN speed: `docker exec gluetun wget -qO- https://speed.cloudflare.com/__down?measId=10000000 > /dev/null` — if it's very slow> **Disk space filling up:** - By default, qBittorrent keeps torrents after Radarr/Sonarr imports them. To auto-clean: - In Radarr/Sonarr → Settings → Download Clients → click on qBittorrent → enable **Remove Completed** - This deletes the torrent from qBittorrent after the file has been imported (the hard link in your media folder is kept, so you don't lo> **VPN IP leak — want to make sure your real IP isn't exposed:** ```bash # Check the VPN container's IP (should NOT be your real IP) docker exec gluetun wget -qO- ifconfig.me # Compare with your real IP (run this outside Docker) curl -s ifconfig.me ``` If both IPs are the same, your VPN isn't working — check Gluetun logs with `docker logs gluetun`. ## Credits Built by [Tom Spark](https://youtube.com/@tomspark) following [Trash Guides](https://trash-guides.info/) and \[Servarr Wiki\](https://wiki.se> Uses [Gluetun](https://github.com/qdm12/gluetun) for VPN, [LinuxServer.io](https://linuxserver.io) container images, and \[Seerr\](https://gi> # Set up folders Script ``` #!/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},media/{movies,tv,music}} 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" 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 "" ``` # Test Stack script ``` #!/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/media/movies /data/media/tv /data/media/music; 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 ${NC}\n" fi ``` # Troubleshooting Seerr I am installing seerr and am getting the following error. Here is my compose setup: It looks like you forgot to paste the specific text of the error! However, based on the official Seerr image specifications and a common pitfall in your current `compose.yaml` configuration, the issue is almost certainly a **permissions error (Permission Denied / Exit Code 13)** or a **healthcheck failure**. The official Seerr image runs natively as a non-root user (**node** with **UID/GID 1000**), which completely alters how it handles local directory volumes. Here are the primary fixes to get your setup running perfectly: Because Seerr runs as UID `1000`, it cannot read or write to your configuration folder if it was originally created by `root` (which Docker often does when initializing empty volumes/directories). To fix this on your host machine, run the following command to hand ownership over to UID 1000:
Bash
``` sudo chown -R 1000:1000 /path/to/your/seerr_config ```
*(Replace `/path/to/your/seerr_config` with the actual path on your host machine where that named volume or directory resides).* ## 2. Adjust the Healthcheck Route In your healthcheck block, you are checking the endpoint `/api/v1/settings/public`. While this endpoint exists, Seerr provides a dedicated health API endpoint designed specifically for containers to probe. Change your healthcheck test to target `/api/v1/status`:
YAML
``` healthcheck: test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/status || exit 1 start_period: 20s timeout: 3s interval: 15s retries: 3 ```