--- tags: - Docker - Media --- 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.