# Tom Spark's ARR Stack — Automated Media Server # https://github.com/loponai/arrstack # # Usage: # 1. Copy .env.example to .env and fill in your VPN credentials # 2. Run: bash setup-folders.sh # 3. Run: docker compose up -d # # All VPN-protected services (qBittorrent, Prowlarr, FlareSolverr) run # through Gluetun. If the VPN drops, traffic stops. Zero leaks. # # Radarr, Sonarr, Lidarr, Bazarr, Jellyfin, and Seerr do NOT run through # the VPN — they need direct network access for speed and local connectivity. networks: arrnetwork: name: arrnetwork ipam: config: - subnet: 172.39.0.0/24 services: # ============================================================ # GLUETUN — VPN Container (kill switch + tunnel) # All VPN-protected services route through this container. # Ports for those services are mapped HERE, not on the services themselves. # Docs: https://github.com/qdm12/gluetun-wiki # ============================================================ gluetun: image: qmcgaw/gluetun:latest container_name: gluetun cap_add: - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun networks: arrnetwork: ipv4_address: ${IP_GLUETUN} ports: - 8000:8000 # Gluetun Control Server - 8080:8080 # qBittorrent WebUI - 6881:6881 # qBittorrent torrenting port - 6881:6881/udp - 9696:9696 # Prowlarr - 8191:8191 # FlareSolverr volumes: - gluetun_volume:/gluetun environment: - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER} - VPN_TYPE=${VPN_TYPE} # --- WireGuard credentials (most providers) --- - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY} - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES} # - WIREGUARD_PUBLIC_KEY=${WIREGUARD_PUBLIC_KEY} - WIREGUARD_PRESHARED_KEY=${WIREGUARD_PRESHARED_KEY} # --- OpenVPN credentials (if using OpenVPN instead) --- # - OPENVPN_USER=${OPENVPN_USER} # - OPENVPN_PASSWORD=${OPENVPN_PASSWORD} # --- Server selection --- - SERVER_COUNTRIES=${SERVER_COUNTRIES} # --- Port forwarding (ProtonVPN, AirVPN, PIA) --- # - VPN_PORT_FORWARDING=${VPN_PORT_FORWARDING} - FIREWALL_VPN_INPUT_PORTS=${FIREWALL_VPN_INPUT_PORTS} - FIREWALL_OUTBOUND_SUBNETS=192.168.0.0/22 # --- General --- - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} - BLOCK_MALICIOUS=off - HTTP_CONTROL_SERVER_ADDRESS=:8000 - HTTP_CONTROL_SERVER_LOG=on - HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE={"auth":"none"} healthcheck: test: wget -qO /dev/null http://127.0.0.1:9999 || exit 1 interval: 20s timeout: 10s retries: 5 restart: unless-stopped # ============================================================ # QBITTORRENT — Torrent Client (runs through Gluetun VPN) # ALL traffic goes through the VPN tunnel. No direct internet. # ============================================================ qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent network_mode: service:gluetun depends_on: gluetun: condition: service_healthy restart: true labels: - deunhealth.restart.on.unhealthy=true environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} - WEBUI_PORT=8080 - TORRENTING_PORT=${FIREWALL_VPN_INPUT_PORTS} volumes: - qbittorrent_volume:/config - /data:/data healthcheck: test: wget -q --spider http://localhost:8080 || exit 1 interval: 60s timeout: 10s retries: 3 start_period: 20s restart: unless-stopped # ============================================================ # DEUNHEALTH — Auto-restarts unhealthy containers # If qBittorrent loses VPN connection, this restarts it automatically. # ============================================================ deunhealth: image: qmcgaw/deunhealth container_name: deunhealth network_mode: none environment: - LOG_LEVEL=info - HEALTH_SERVER_ADDRESS=127.0.0.1:9999 - TZ=${TZ} volumes: - /var/run/docker.sock:/var/run/docker.sock - deunhealth_volume:/config restart: always # ============================================================ # PROWLARR — Indexer Manager (runs through Gluetun VPN) # Manages torrent/usenet indexers. Syncs to Radarr/Sonarr/Lidarr. # ============================================================ prowlarr: image: lscr.io/linuxserver/prowlarr:latest container_name: prowlarr network_mode: service:gluetun depends_on: gluetun: condition: service_healthy restart: true environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - prowlarr_volume:/config restart: unless-stopped # ============================================================ # FLARESOLVERR — Cloudflare Bypass (runs through Gluetun VPN) # Some indexers use Cloudflare protection. This gets around it. # ============================================================ flaresolverr: image: ghcr.io/flaresolverr/flaresolverr:latest container_name: flaresolverr network_mode: service:gluetun depends_on: gluetun: condition: service_healthy restart: true volumes: - flaresolverr_volume:/config environment: - LOG_LEVEL=info - TZ=${TZ} restart: unless-stopped # ============================================================ # RADARR — Movie Manager (NOT behind VPN) # Searches via Prowlarr, sends downloads to qBittorrent, # renames and hard-links completed files to media folder. # ============================================================ radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - radarr_volume:/config - /data:/data ports: - 7878:7878 networks: arrnetwork: ipv4_address: ${IP_RADARR} restart: unless-stopped # ============================================================ # SONARR — TV Show Manager (NOT behind VPN) # Same pattern as Radarr but for TV series. # ============================================================ sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - sonarr_volume:/config - /data:/data ports: - 8989:8989 networks: arrnetwork: ipv4_address: ${IP_SONARR} restart: unless-stopped # ============================================================ # LIDARR — Music Manager (NOT behind VPN) # Optional. Comment out if you don't need music automation. # ============================================================ lidarr: image: lscr.io/linuxserver/lidarr:latest container_name: lidarr environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - lidarr_volume:/config - /data:/data ports: - 8686:8686 networks: arrnetwork: ipv4_address: ${IP_LIDARR} restart: unless-stopped # ============================================================ # BAZARR — Subtitle Manager (NOT behind VPN) # Automatically downloads subtitles for movies and TV shows. # ============================================================ bazarr: image: lscr.io/linuxserver/bazarr:latest container_name: bazarr environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - bazarr_volume:/config - /data:/data ports: - 6767:6767 networks: arrnetwork: ipv4_address: ${IP_BAZARR} restart: unless-stopped # ============================================================ # JELLYFIN — Media Server (NOT behind VPN) # Your personal streaming service. Plays movies, TV, music. # Needs full bandwidth — never put this behind the VPN. # ============================================================ jellyfin: image: lscr.io/linuxserver/jellyfin:latest container_name: jellyfin environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - jellyfin_volume:/config - /data/media:/data/media ports: - 8096:8096 # Uncomment the lines below to enable hardware transcoding (Intel Quick Sync / VAAPI). # Only works if your system has Intel/AMD integrated graphics (/dev/dri must exist). # If you get an error about /dev/dri not found, leave these commented out. # devices: # - /dev/dri:/dev/dri networks: arrnetwork: ipv4_address: ${IP_JELLYFIN} restart: unless-stopped # ============================================================ # SEERR — Request System (NOT behind VPN) # Netflix-like UI for requesting movies and TV shows. # Share this with family — they never need to touch Radarr. # # Seerr is the unified successor to Overseerr and Jellyseerr # (merged under seerr-team). Supports Plex, Jellyfin, and Emby. # # Config uses a NAMED Docker volume (not a bind mount). This is # required: Seerr runs as the `node` user (UID 1000) and a # bind-mounted host folder is created root-owned, causing a # permission-denied crash loop. On Windows/WSL, bind mounts also # corrupt the SQLite DB over SMB. Named volumes fix both cases # (matches upstream Seerr docs). # # Migrating from ./jellyseerr or ./seerr bind mount? See README # troubleshooting "Migrating Seerr config to a named volume". # ============================================================ seerr: image: ghcr.io/seerr-team/seerr:v3.0.1 init: true container_name: seerr environment: - LOG_LEVEL=info - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} - PORT=5055 volumes: - seerr_volume:/app/config # <--- Needed or will cause a permissions issue. ports: - 5055:5055 healthcheck: test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/settings/public || exit 1 start_period: 20s timeout: 3s interval: 15s retries: 3 networks: arrnetwork: ipv4_address: ${IP_SEERR} restart: unless-stopped listenarr: image: ghcr.io/listenarrs/listenarr:canary container_name: listenarr ports: - "4545:4545" environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - listenarr_volume:/app/config - /data/media/audiobookshelf/books:/audiobooks - /data/torrents/books:/downloads restart: unless-stopped networks: arrnetwork: ipv4_address: ${IP_LIST} audiobookshelf: image: ghcr.io/advplyr/audiobookshelf:latest container_name: audiobookshelf ports: - 13378:80 environment: - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - /data/media/books/audiobookshelf/books:/audiobooks - /data/media/books/audiobookshelf/podcasts:/podcasts - /data/media/books/audiobookshelf/metadata:/metadata - audiobookshelf_volume:/config restart: unless-stopped networks: arrnetwork: ipv4_address: ${IP_AUDIO} navidrome: image: deluan/navidrome:latest container_name: navidrome ports: - 4533:4533 restart: unless-stopped environment: - ND_SCANSCHEDULE=1h - ND_LOGLEVEL=info - ND_SESSIONTIMEOUT=24h - PUID=${PUID} - PGID=${PGID} - UMASK=002 - TZ=${TZ} volumes: - navidrome_volume:/data - /data/media/music:/music:ro networks: arrnetwork: ipv4_address: ${IP_NAVI} watchtower: image: containrrr/watchtower container_name: watchtower restart: unless-stopped env_file: .env environment: - DOCKER_API_VERSION=1.44 - TZ=${TZ} - PUID:=${PUID} # Reference PUID from .env - PGID=${PGID} # Reference PGID from .env - WATCHTOWER_CLEANUP=${WATCHTOWER_CLEANUP} - WATCHTOWER_INCLUDE_RESTARTING=${WATCHTOWER_INCLUDE_RESTARTING} - WATCHTOWER_ROLLING_RESTARTING=${WATCHTOWER_ROLLING_RESTARTING} - WATCHTOWER_SCHEDULE=${WATCHTOWER_SCHEDULE} - WATCHTOWER_INCLUDE_STOPPED=${WATCHTOWER_INCLUDE_STOPPED} - WATCHTOWER_NOTIFICATIONS=${WATCHTOWER_NOTIFICATIONS} - WATCHTOWER_NOTIFICATIONS_HOSTNAME=${WATCHTOWER_NOTIFICATIONS_HOSTNAME} - WATCHTOWER_NOTIFICATION_EMAIL_FROM=${WATCHTOWER_NOTIFICATION_EMAIL_FROM} - WATCHTOWER_NOTIFICATION_EMAIL_TO=${WATCHTOWER_NOTIFICATION_EMAIL_TO} - WATCHTOWER_NOTIFICATION_EMAIL_SERVER=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER} - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT} - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER} - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=${WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD} security_opt: - no-new-privileges:true volumes: - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro - /var/run/docker.sock:/var/run/docker.sock - watchtower_volume:/config networks: arrnetwork: ipv4_address: ${IP_WATCH} # Go to the VERY BOTTOM of your file (outside of services) and add this: volumes: gluetun_volume: qbittorrent_volume: deunhealth_volume: prowlarr_volume: flaresolverr_volume: radarr_volume: sonarr_volume: lidarr_volume: bazarr_volume: jellyfin_volume: seerr_volume: listenarr_volume: audiobookshelf_volume: navidrome_volume: watchtower_volume: