1569 lines
56 KiB
Markdown
1569 lines
56 KiB
Markdown
---
|
|
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
|
|
|
|
<table id="bkmrk-service-port-purpose"><thead><tr><th>Service</th><th>Port</th><th>Purpose</th></tr></thead><tbody><tr><td>Gluetun</td><td>—</td><td>VPN tunnel with kill switch</td></tr><tr><td>qBittorrent</td><td>8080</td><td>Torrent client (VPN protected)</td></tr><tr><td>Prowlarr</td><td>9696</td><td>Indexer manager (VPN protected)</td></tr><tr><td>FlareSolverr</td><td>8191</td><td>Cloudflare bypass (VPN protected)</td></tr><tr><td>Radarr</td><td>7878</td><td>Movie automation</td></tr><tr><td>Sonarr</td><td>8989</td><td>TV show automation</td></tr><tr><td>Lidarr</td><td>8686</td><td>Music automation</td></tr><tr><td>Bazarr</td><td>6767</td><td>Subtitle automation</td></tr><tr><td>Jellyfin</td><td>8096</td><td>Media server / streaming</td></tr><tr><td>Seerr</td><td>5055</td><td>Netflix-like request UI (Overseerr/Jellyseerr successor)</td></tr></tbody></table>
|
|
|
|
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):**
|
|
|
|
<table id="bkmrk-ip-service-172.39.0."><thead><tr><th>IP</th><th>Service</th></tr></thead><tbody><tr><td>`172.39.0.2`</td><td>Gluetun (also qBittorrent, Prowlarr, FlareSolverr)</td></tr><tr><td>`172.39.0.3`</td><td>Radarr</td></tr><tr><td>`172.39.0.4`</td><td>Sonarr</td></tr><tr><td>`172.39.0.5`</td><td>Lidarr</td></tr><tr><td>`172.39.0.6`</td><td>Bazarr</td></tr><tr><td>`172.39.0.7`</td><td>Jellyfin</td></tr><tr><td>`172.39.0.8`</td><td>Seerr</td></tr></tbody></table>
|
|
|
|
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\](<a class="external-link" target="_blank">https://get.s></a>
|
|
|
|
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 <PID> | 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 <ServiceName> -Force
|
|
Set-Service <ServiceName> -StartupType Disabled
|
|
|
|
```
|
|
4. Then recreate the container:
|
|
|
|
```bash
|
|
docker compose up -d --force-recreate <service-name>
|
|
|
|
```
|
|
|
|
**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\](<a class="external-link" target="_blank">https://wiki.se></a>
|
|
|
|
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 <container-name>${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:
|
|
|
|
<div class="code-block ng-tns-c602925248-25 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" id="bkmrk-bash"><div class="formatted-code-block-internal-container ng-tns-c602925248-25"><div class="animated-opacity ng-tns-c602925248-25"><div class="code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c602925248-25 ng-star-inserted"><span class="ng-tns-c602925248-25">Bash</span><div class="buttons ng-tns-c602925248-25 ng-star-inserted"></div></div></div></div></div>```
|
|
sudo chown -R 1000:1000 /path/to/your/seerr_config
|
|
|
|
```
|
|
|
|
<div class="code-block ng-tns-c602925248-25 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" id="bkmrk--1"><div class="formatted-code-block-internal-container ng-tns-c602925248-25"><div class="animated-opacity ng-tns-c602925248-25"></div></div></div>*(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`:
|
|
|
|
<div class="code-block ng-tns-c602925248-26 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" id="bkmrk-yaml"><div class="formatted-code-block-internal-container ng-tns-c602925248-26"><div class="animated-opacity ng-tns-c602925248-26"><div class="code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c602925248-26 ng-star-inserted"><span class="ng-tns-c602925248-26">YAML</span><div class="buttons ng-tns-c602925248-26 ng-star-inserted"></div></div></div></div></div>```
|
|
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
|
|
``` |