---
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.