The Complete Mediastack Guide
1. What Is a Mediastack?
Section titled “1. What Is a Mediastack?”A mediastack is a collection of self-hosted applications that work together to:
- Find media (movies, TV shows, music) on torrent indexers
- Download it through a VPN-protected torrent client
- Organize it into a clean library with proper naming
- Serve it to your devices via a media server (like Plex or Jellyfin)
- Fetch subtitles automatically
- Let users request new content from a friendly web UI
All of this happens automatically. You (or your family) request a movie, and it appears in Plex within minutes — downloaded, renamed, moved to the right folder, with subtitles.
The apps and what they do
Section titled “The apps and what they do”| App | Role | One-sentence summary |
|---|---|---|
| Gluetun | VPN container | Runs a VPN tunnel that the torrent client routes traffic through |
| qBittorrent | Download client | Downloads torrents through the VPN |
| Prowlarr | Indexer manager | Connects to torrent sites and shares them with Sonarr/Radarr/Lidarr |
| Sonarr | TV show manager | Monitors TV shows, searches for new episodes, sends them to qBittorrent |
| Radarr | Movie manager | Same as Sonarr but for movies |
| Lidarr | Music manager | Same as Sonarr but for music albums |
| Bazarr | Subtitle manager | Automatically downloads subtitles for everything Sonarr and Radarr grab |
| Plex | Media server | Serves your library to TVs, phones, and browsers |
| Seerr | Request manager | Pretty UI where users can browse and request movies/shows |
| Recyclarr | Quality sync | Automatically applies community-recommended quality profiles |
| FlareSolverr | CAPTCHA solver | Helps Prowlarr access indexers that use Cloudflare protection |
2. How the Pieces Fit Together
Section titled “2. How the Pieces Fit Together”Here’s the flow when someone requests a movie:
User opens Seerr → "I want to watch Interstellar" │ ▼Seerr sends request to Radarr │ ▼Radarr asks Prowlarr: "Find Interstellar in 1080p Bluray" │ ▼Prowlarr searches your configured torrent indexers │ ▼Prowlarr returns results to Radarr │ ▼Radarr picks the best match and sends it to qBittorrent │ ▼qBittorrent downloads it through Gluetun (VPN-protected) │ ▼qBittorrent finishes → Radarr detects completion │ ▼Radarr hardlinks/moves the file to your media library: /data/media/movies/Interstellar (2014)/Interstellar (2014).mkv │ ▼Plex detects the new file and adds it to your library │ ▼Bazarr detects the new movie and downloads subtitles │ ▼User gets a notification and watches InterstellarThe network architecture
Section titled “The network architecture”The stack uses a split-network topology: only the torrent client (qBittorrent) runs behind the VPN. The *arr apps (Sonarr, Radarr, Prowlarr, etc.) run on a separate bridge network with direct internet access.
Why not put everything behind the VPN? Routing all traffic through the VPN causes problems:
- Cloudflare blocks — indexers and metadata APIs see VPN IPs as suspicious and block requests
- Rate-limiting — metadata providers (TMDB, TVDB, MusicBrainz) rate-limit VPN IP ranges heavily
- Private tracker bans — some trackers ban VPN IP ranges from their API/web UI (while allowing them for torrent traffic)
- Single point of failure — if the VPN drops, every service goes down, not just torrents
- No benefit — the *arr apps don’t need VPN protection. They only talk to APIs. Only the actual torrent traffic needs to be anonymous.
┌────────────────────────────────────────────────────────────────────────┐│ mediastack network (bridge) ││ ││ ┌─────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌───────┐ ││ │ Sonarr │ │ Radarr │ │ Lidarr │ │Prowlarr│ │ Bazarr │ │Autobrr│ ││ │ :8989 │ │ :7878 │ │ :8686 │ │ :9696 │ │ :6767 │ │ :7474│ ││ └────┬────┘ └────┬────┘ └────┬───┘ └────┬───┘ └────┬───┘ └───┬───┘ ││ │ │ │ │ │ │ ││ └───────────┴───────────┴─────┬─────┴──────────┴──────────┘ ││ │ ││ gluetun_vpn:8090 ││ (reach qBit via Gluetun) ││ │ │└─────────────────────────────────────┼──────────────────────────────────┘ │┌─────────────────────────────────────┼──────────────────────────────────┐│ VPN network │ ││ │ ││ ┌──────────┐ ┌──────┴─────┐ ││ │ Gluetun │◄── VPN tunnel │qBittorrent │ ││ │ (gateway)│ │ :8090 │ ││ └──────────┘ └────────────┘ ││ │└────────────────────────────────────────────────────────────────────────┘Gluetun sits on both networks — it’s the bridge. The *arr apps reach qBittorrent by connecting to gluetun:8090 (Gluetun’s address on the mediastack network, which forwards to qBittorrent running in its network namespace). When Sonarr searches an indexer, it goes directly to the internet with your real IP. When qBittorrent downloads a torrent, it goes through the VPN tunnel. Best of both worlds.
3. Prerequisites
Section titled “3. Prerequisites”Hardware
Section titled “Hardware”- CPU: Any modern x86_64 CPU. Hardware transcoding in Plex benefits from an Intel CPU with Quick Sync (any 7th gen+ Intel) or a dedicated GPU.
- RAM: 8 GB minimum, 16 GB recommended. The *arr apps are memory-hungry.
- Storage: As much as you can get. A NAS with multiple terabytes is ideal. Media libraries grow fast.
Software
Section titled “Software”- Docker Engine and Docker Compose v2 installed
- Linux host (Debian, Ubuntu, or similar). This guide assumes Linux.
Accounts
Section titled “Accounts”- VPN subscription — This guide uses Private Internet Access (PIA) because it supports port forwarding (important for seeding). Other good options: Mullvad, Windscribe, AirVPN. Check Gluetun’s supported providers.
- Torrent indexer accounts — You need at least one torrent indexer. Public indexers work but private trackers have better quality and speed. Prowlarr supports hundreds of indexers.
- Plex account — Free at plex.tv. You’ll need a claim token for first setup.
User/Group IDs
Section titled “User/Group IDs”Every container needs to run with the same user and group IDs so file permissions work. Find yours:
id# Output: uid=1000(youruser) gid=1000(youruser) groups=...Note your uid (PUID) and gid (PGID). You’ll use these in every container.
4. Folder Structure (This Is Critical)
Section titled “4. Folder Structure (This Is Critical)”This is the single most important section of this guide. Get the folder structure wrong and you’ll have broken hardlinks, double disk usage, and slow imports. Get it right and everything just works.
The problem with separate mounts
Section titled “The problem with separate mounts”Many guides mount /downloads and /media as separate volumes in each container. This breaks hardlinks. When Sonarr “moves” a completed download to your library, it should create a hardlink (same file, two paths, zero extra disk space). But hardlinks only work within the same filesystem/mount. Separate mounts = separate filesystems = no hardlinks = files get copied instead, using double the disk space.
The solution: one shared mount
Section titled “The solution: one shared mount”All containers get the same top-level /data mount. Inside it, downloads and media are subdirectories:
/data/ ← One mount, shared by ALL containers├── media_stack/│ └── qbittorrent/│ ├── movies/ ← qBittorrent downloads movies here│ ├── tv/ ← qBittorrent downloads TV here│ └── music/ ← qBittorrent downloads music here└── media/ ├── movies/ ← Radarr's library (hardlinked from above) │ └── Interstellar (2014)/ │ └── Interstellar (2014).mkv ├── tv/ ← Sonarr's library │ └── Breaking Bad/ │ └── Season 01/ │ └── Breaking Bad - S01E01 - Pilot.mkv └── music/ ← Lidarr's library └── Pink Floyd/ └── The Dark Side of the Moon (1973)/ └── 01 - Speak to Me.flacCreate the structure
Section titled “Create the structure”# Create the directory tree# Adjust the base path to wherever your storage issudo mkdir -p /data/media_stack/qbittorrent/{movies,tv,music}sudo mkdir -p /data/media/{movies,tv,music}
# Set ownership to your PUID:PGIDsudo chown -R 1000:1000 /dataHow each container sees it
Section titled “How each container sees it”Every container mounts the same /data directory:
volumes: - /path/to/your/storage:/data # Same mount in EVERY container| Container | What it uses inside /data |
|---|---|
| qBittorrent | Downloads to /data/media_stack/qbittorrent/{movies,tv,music} |
| Sonarr | Sees downloads at /data/media_stack/qbittorrent/tv, library at /data/media/tv |
| Radarr | Sees downloads at /data/media_stack/qbittorrent/movies, library at /data/media/movies |
| Lidarr | Sees downloads at /data/media_stack/qbittorrent/music, library at /data/media/music |
| Plex | Reads from /data/media/{movies,tv,music} |
Because they all share the same mount, hardlinks work. When Radarr imports a completed movie, it creates a hardlink from the download folder to the library folder — instant, zero extra disk space. The original file stays for seeding, the library copy is the same file on disk.
5. The VPN — Gluetun
Section titled “5. The VPN — Gluetun”Gluetun is a lightweight container that establishes a VPN tunnel. The torrent client connects to the internet through Gluetun by using Docker’s network_mode: service:gluetun.
What it does
Section titled “What it does”- Connects to your VPN provider (PIA, Mullvad, etc.)
- Creates a tunnel interface (
tun0) - Acts as a gateway for the torrent client sharing its network namespace
- Sits on both the VPN network and the mediastack network, so *arr apps can reach qBittorrent through it
- Optionally handles port forwarding (critical for seeding on private trackers)
- Has a built-in control API on port 8000 that reports the VPN status and forwarded port
Why only the torrent client needs VPN
Section titled “Why only the torrent client needs VPN”Only qBittorrent handles actual torrent traffic (peer-to-peer connections where your IP is visible to other peers). The *arr apps only make HTTPS API calls to indexers and metadata providers — these are regular web requests that don’t expose your activity. Keeping *arr apps off the VPN gives them:
- Faster, more reliable access to metadata APIs (TMDB, TVDB, MusicBrainz)
- No Cloudflare blocks on indexer web UIs
- No VPN rate-limiting from metadata providers
- Independent uptime — VPN issues only affect downloads, not your entire stack
Why port forwarding matters
Section titled “Why port forwarding matters”Without port forwarding, you can download but can’t seed properly. Other peers can’t initiate connections to you, so your upload speed (and ratio on private trackers) suffers. PIA, Mullvad, and AirVPN support port forwarding through Gluetun.
Configuration
Section titled “Configuration”gluetun: image: qmcgaw/gluetun:v3 container_name: gluetun restart: unless-stopped cap_add: - NET_ADMIN # Required — Gluetun creates network interfaces environment: - VPN_SERVICE_PROVIDER=private internet access - OPENVPN_USER=your-vpn-username - OPENVPN_PASSWORD=your-vpn-password - SERVER_REGIONS=Netherlands # Pick a region close to you - PORT_FORWARD_ONLY=true # Only connect to servers that support port forwarding - VPN_PORT_FORWARDING=on # Enable port forwarding - UPDATER_PERIOD=24h # Update server list daily - TZ=Europe/Copenhagen volumes: - /srv/docker/gluetun:/gluetun # VPN state/config ports: # Only qBittorrent ports — the torrent client runs in Gluetun's # network namespace, so its ports are exposed here. - "8090:8090" # qBittorrent Web UI - "6881:6881" # qBittorrent torrent port - "6881:6881/udp" networks: - vpn # Internal VPN network for qBittorrent - mediastack # Bridge network so *arr apps can reach qBit via gluetun:8090Port forwarding sync
Section titled “Port forwarding sync”When your VPN provider assigns a forwarded port, qBittorrent needs to know about it. Gluetun’s control API (http://localhost:8000/v1/openvpn/portforwarded) reports the current port. A simple script syncs it to qBittorrent:
#!/bin/bash# port-sync.sh — run every 5 minutes via cron or as a containerGLUETUN_API="http://localhost:8000"QBIT_API="http://localhost:8090"QBIT_USER="admin"QBIT_PASS="your-qbit-password"
# Get forwarded port from GluetunPORT=$(curl -sf "${GLUETUN_API}/v1/openvpn/portforwarded" | jq -r '.port')
if [ -z "$PORT" ] || [ "$PORT" = "0" ]; then echo "No forwarded port available" exit 1fi
# Log into qBittorrentCOOKIE=$(curl -sf -c - "${QBIT_API}/api/v2/auth/login" \ --data "username=${QBIT_USER}&password=${QBIT_PASS}" | grep SID | awk '{print $NF}')
# Set the listening portcurl -sf "${QBIT_API}/api/v2/app/setPreferences" \ --cookie "SID=${COOKIE}" \ --data "json={\"listen_port\":${PORT}}"
echo "Set qBittorrent listening port to ${PORT}"This can run as a sidecar container in your compose file using network_mode: service:gluetun (it needs localhost access to both Gluetun’s API and qBittorrent).
6. The Download Client — qBittorrent
Section titled “6. The Download Client — qBittorrent”qBittorrent does the actual downloading. It’s the only service that runs behind Gluetun, because it’s the only one doing peer-to-peer torrent traffic where your IP is visible.
Configuration
Section titled “Configuration”qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent restart: unless-stopped network_mode: service:gluetun # Route ALL traffic through Gluetun depends_on: gluetun: condition: service_healthy environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen - UMASK=022 - WEBUI_PORT=8090 # Web UI port (exposed on Gluetun) volumes: - /srv/docker/qbittorrent:/config # qBittorrent config - /data:/data # Shared data mountNotice: no ports: section. qBittorrent’s ports are exposed on the Gluetun container instead (see section 5).
network_mode: service:gluetun means qBittorrent shares Gluetun’s network stack. When qBittorrent connects to a peer, it goes through Gluetun’s VPN tunnel. When you access http://your-server:8090, the request goes to Gluetun’s port 8090 which routes to qBittorrent.
First-time setup
Section titled “First-time setup”- Open
http://your-server:8090 - Default login: check the container logs for the temporary password (
docker logs qbittorrent) - Change the password immediately in Settings → Web UI
- Go to Settings → Downloads: - Default Save Path:
/data/media_stack/qbittorrent/ - Go to Settings → BitTorrent: - Enable DHT, PeX, and Local Peer Discovery (unless your private trackers forbid it) - Set global upload/download limits if needed
Categories
Section titled “Categories”Sonarr, Radarr, and Lidarr automatically create their categories in qBittorrent when you add qBit as a download client and specify a category name (e.g., tv, movies, music). You don’t need to create them manually — the *arr apps handle it.
| App | Category | Save Path |
|---|---|---|
| Sonarr | tv | /data/media_stack/qbittorrent/tv |
| Radarr | movies | /data/media_stack/qbittorrent/movies |
| Lidarr | music | /data/media_stack/qbittorrent/music |
The save paths are set in qBittorrent’s category settings after the *arr app creates them. Or you can set qBittorrent’s Default Save Path to /data/media_stack/qbittorrent/ and the categories will use subdirectories automatically.
7. The Indexer Manager — Prowlarr
Section titled “7. The Indexer Manager — Prowlarr”Prowlarr is the central place where you configure your torrent indexers (tracker sites). Instead of adding each indexer separately to Sonarr, Radarr, and Lidarr, you add them once in Prowlarr and it syncs them to all the *arr apps.
Configuration
Section titled “Configuration”prowlarr: image: lscr.io/linuxserver/prowlarr:latest container_name: prowlarr restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen volumes: - /srv/docker/prowlarr/config:/config ports: - "9696:9696" networks: - mediastackFirst-time setup
Section titled “First-time setup”- Open
http://your-server:9696 - Set up authentication (Settings → General → Authentication)
- Add indexers (Indexers → Add Indexer): - Search for your tracker name - Enter your credentials/API key/cookie - Test the connection
- Connect Prowlarr to your *arr apps (Settings → Apps → Add Application): - Add Sonarr: Prowlarr URL
http://prowlarr:9696, Sonarr URLhttp://sonarr:8989, API key from Sonarr - Add Radarr: Prowlarr URLhttp://prowlarr:9696, Radarr URLhttp://radarr:7878, API key from Radarr - Add Lidarr: Prowlarr URLhttp://prowlarr:9696, Lidarr URLhttp://lidarr:8686, API key from Lidarr - Add download client: Settings → Download Clients → qBittorrent - Host:
gluetun(NOTlocalhost— qBittorrent is behind the VPN, reachable via Gluetun on the mediastack network) - Port:8090
FlareSolverr
Section titled “FlareSolverr”Some indexers use Cloudflare protection. FlareSolverr solves CAPTCHAs for Prowlarr:
flaresolverr: image: ghcr.io/flaresolverr/flaresolverr:latest container_name: flaresolverr restart: unless-stopped environment: - TZ=Europe/Copenhagen ports: - "8191:8191" networks: - mediastackIn Prowlarr: Settings → Indexers → Add → FlareSolverr → URL: http://flaresolverr:8191
8. TV Shows — Sonarr
Section titled “8. TV Shows — Sonarr”Sonarr manages your TV show library. You tell it which shows you want, and it monitors indexers for new episodes, downloads them, renames them, and moves them to your library.
Configuration
Section titled “Configuration”sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen - UMASK=022 volumes: - sonarr_config:/config # Local volume (see note below) - /data:/data # Shared data mount ports: - "8989:8989" networks: - mediastackFirst-time setup
Section titled “First-time setup”- Open
http://your-server:8989 - Set authentication: Settings → General → Authentication → Forms
- Add root folder: Settings → Media Management → Root Folders → Add →
/data/media/tv - Add download client: Settings → Download Clients → Add → qBittorrent - Host:
gluetun(qBittorrent is behind the VPN, reachable via Gluetun) - Port:8090- Username/password: your qBittorrent credentials - Category:tv - Note your API key: Settings → General → API Key (you’ll need this for Prowlarr and Bazarr)
- Add a show: Series → Add New → search and add
How Sonarr names files
Section titled “How Sonarr names files”Sonarr renames files on import using a configurable pattern. The default is fine for most people:
Breaking Bad - S01E01 - Pilot.mkvYou can customize this in Settings → Media Management → Episode Naming.
How importing works
Section titled “How importing works”- qBittorrent downloads an episode to
/data/media_stack/qbittorrent/tv/ - Sonarr detects the completed download (it polls qBittorrent every 60 seconds)
- Sonarr creates a hardlink from the download to
/data/media/tv/Show Name/Season XX/ - The file now exists at both paths but uses disk space only once
- The original stays in the download folder for seeding
- When seeding is done (or after a configurable delay), Sonarr can optionally delete the original
9. Movies — Radarr
Section titled “9. Movies — Radarr”Radarr is Sonarr but for movies. The setup is nearly identical.
Configuration
Section titled “Configuration”radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen - UMASK=022 volumes: - radarr_config:/config - /data:/data ports: - "7878:7878" networks: - mediastackFirst-time setup
Section titled “First-time setup”- Open
http://your-server:7878 - Set authentication: Settings → General → Authentication → Forms
- Add root folder: Settings → Media Management → Root Folders → Add →
/data/media/movies - Add download client: Settings → Download Clients → Add → qBittorrent - Host:
gluetun, Port:8090, Category:movies - Note your API key for Prowlarr and Bazarr
- Add a movie: Movies → Add New
Quality profiles
Section titled “Quality profiles”Radarr comes with quality profiles that determine which releases to grab (e.g., prefer Bluray 1080p over WEB-DL). The defaults are okay, but Recyclarr (section 14) can sync community-recommended profiles from TRaSH Guides, which are much better.
10. Music — Lidarr
Section titled “10. Music — Lidarr”Lidarr manages your music library. Same pattern as Sonarr/Radarr.
Configuration
Section titled “Configuration”lidarr: image: lscr.io/linuxserver/lidarr:latest container_name: lidarr restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen - UMASK=022 volumes: - /srv/docker/lidarr/config:/config - /data:/data ports: - "8686:8686" networks: - mediastackFirst-time setup
Section titled “First-time setup”- Open
http://your-server:8686 - Set authentication
- Add root folder:
/data/media/music - Add download client: qBittorrent on
gluetun:8090, category:music - Add an artist: Artist → Add New
11. Subtitles — Bazarr
Section titled “11. Subtitles — Bazarr”Bazarr automatically downloads subtitles for your movies and TV shows. It integrates with Sonarr and Radarr — when a new file is imported, Bazarr checks for subtitles.
Configuration
Section titled “Configuration”bazarr: image: lscr.io/linuxserver/bazarr:latest container_name: bazarr restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen - UMASK=022 volumes: - /srv/docker/bazarr/config:/config - /data:/data ports: - "6767:6767" networks: - mediastackFirst-time setup
Section titled “First-time setup”- Open
http://your-server:6767 - Connect to Sonarr: Settings → Sonarr → URL:
http://sonarr:8989, API key - Connect to Radarr: Settings → Radarr → URL:
http://radarr:7878, API key - Add subtitle providers: Settings → Providers - OpenSubtitles.com — free account, decent coverage - Addic7ed — good for TV shows - Gestdown — alternative
- Set languages: Settings → Languages → add your preferred languages
- Enable automatic search: Settings → Sonarr/Radarr → toggle “Search for Missing Subtitles”
Bazarr scores subtitle matches and picks the best one. It can also upgrade subtitles if a better match appears later.
12. The Media Server — Plex
Section titled “12. The Media Server — Plex”Plex serves your media library to your devices. It runs on the host network for best performance and DLNA support.
Configuration
Section titled “Configuration”plex: image: lscr.io/linuxserver/plex:latest container_name: plex restart: unless-stopped network_mode: host # Direct access to network (no Docker NAT) environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen - VERSION=docker - PLEX_CLAIM=claim-xxxxxxxxxxxx # Get from https://plex.tv/claim volumes: - /srv/docker/plex/config:/config - /data/media:/data/media # Read-only access to media library devices: - /dev/dri:/dev/dri # GPU for hardware transcoding (Intel)Key points
Section titled “Key points”network_mode: host— Plex runs directly on the host network. This is recommended for discovery (DLNA, GDM) and avoids Docker NAT issues. Plex uses port 32400.PLEX_CLAIM— Get a claim token from plex.tv/claim. It expires after 4 minutes, so paste it and start the container quickly./dev/dri— This passes through the GPU for hardware transcoding. Works with Intel Quick Sync (most Intel CPUs), AMD, or NVIDIA. Without this, Plex uses CPU transcoding which is much slower.
First-time setup
Section titled “First-time setup”- Open
http://your-server:32400/web - Sign in with your Plex account
- Add libraries: - Movies:
/data/media/movies- TV Shows:/data/media/tv- Music:/data/media/music - Under each library’s advanced settings, enable “Use original title” and set the scanner/agent to your preference
13. Requests — Seerr
Section titled “13. Requests — Seerr”Seerr (formerly Overseerr/Jellyseerr) is a user-friendly request interface. Instead of telling your family “go to Radarr and add a movie”, you give them a Netflix-like UI where they can browse trending content and click “Request”.
Configuration
Section titled “Configuration”seerr: image: ghcr.io/fallenbagel/jellyseerr:latest container_name: seerr restart: unless-stopped environment: - TZ=Europe/Copenhagen volumes: - /srv/docker/jellyseerr:/app/config ports: - "5055:5055" networks: - proxy # On the proxy network (not behind VPN)First-time setup
Section titled “First-time setup”- Open
http://your-server:5055 - Sign in with your Plex account
- Connect to Plex: enter your server URL
- Connect to Radarr: URL
http://your-server-ip:7878, API key, root folder/data/media/movies - Connect to Sonarr: URL
http://your-server-ip:8989, API key, root folder/data/media/tv
14. Quality Profiles — Recyclarr & TRaSH Guides
Section titled “14. Quality Profiles — Recyclarr & TRaSH Guides”The default quality profiles in Sonarr/Radarr are basic. The community has spent years building TRaSH Guides — optimized quality profiles with custom formats that prefer proper releases, avoid bad encoders, and score releases intelligently.
Recyclarr syncs these profiles to your Sonarr/Radarr automatically.
Configuration
Section titled “Configuration”recyclarr: image: ghcr.io/recyclarr/recyclarr:latest container_name: recyclarr restart: unless-stopped user: 1000:1000 volumes: - /srv/docker/recyclarr:/config networks: - mediastack- First run generates a config template at
/srv/docker/recyclarr/recyclarr.yml - Edit it to connect to your Sonarr and Radarr:
sonarr: main: base_url: http://sonarr:8989 api_key: your-sonarr-api-key quality_definition: type: series quality_profiles: - name: WEB-1080p # TRaSH recommended quality profile for 1080p web releases custom_formats: # Recyclarr docs list which custom formats to include
radarr: main: base_url: http://radarr:7878 api_key: your-radarr-api-key quality_definition: type: movie quality_profiles: - name: HD Bluray + WEB custom_formats: # ...- Run it manually once to verify:
docker exec recyclarr recyclarr sync - After that, it runs automatically on a daily schedule
15. Extras
Section titled “15. Extras”These are optional but useful tools that improve the stack.
Autobrr — Real-time release grabbing
Section titled “Autobrr — Real-time release grabbing”Autobrr monitors IRC announce channels from your private trackers and grabs releases the instant they’re uploaded — before they even appear in RSS feeds. This is how you get the best speeds and build ratio on private trackers.
autobrr: image: ghcr.io/autobrr/autobrr:latest container_name: autobrr restart: unless-stopped user: 1000:1000 volumes: - /srv/docker/autobrr:/config ports: - "7474:7474" networks: - mediastackSetup: http://your-server:7474 → connect to qBittorrent (host: gluetun, port: 8090), add your tracker IRC channels, create filters.
qbit_manage — Torrent housekeeping
Section titled “qbit_manage — Torrent housekeeping”qbit_manage automatically tags, categorizes, and cleans up torrents in qBittorrent. It can remove unregistered torrents, enforce share ratios, organize by tracker, and maintain a recycle bin.
qbit_manage: image: bobokun/qbit_manage:latest container_name: qbit_manage restart: unless-stopped environment: - QBT_RUN=false # Run as daemon (not one-shot) - QBT_SCHEDULE=30 # Run every 30 minutes volumes: - /srv/docker/qbit_manage:/config - /data:/data networks: - mediastackIn config.yml, set the qBittorrent host to gluetun:8090 (since qBit is behind the VPN and reachable via Gluetun on the mediastack network).
Cross-Seed — Find cross-seedable torrents
Section titled “Cross-Seed — Find cross-seedable torrents”Cross-seed scans your existing downloads and finds the same content on other trackers. This lets you seed the same file on multiple trackers without downloading it again, building ratio on new trackers for free.
# Separate stack — /opt/stacks/cross-seed/compose.yamlcross-seed: image: crossseed/cross-seed:latest container_name: cross-seed restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen volumes: - /srv/docker/cross-seed:/config - /srv/docker/cross-seed/cross-seeds:/cross-seeds - /data:/data ports: - "2468:2468"In config.js, use your server’s LAN IP for Prowlarr, Sonarr, Radarr, and qBittorrent URLs (cross-seed runs in a separate stack on the default bridge network, so it can’t use Docker DNS names from the mediastack network).
Unpackerr — Extract archived releases
Section titled “Unpackerr — Extract archived releases”Some trackers upload releases as RAR archives. Unpackerr watches for completed downloads and automatically extracts them so Sonarr/Radarr can import the media files.
# Separate stack — /opt/stacks/unpackerr/compose.yamlunpackerr: image: golift/unpackerr:latest container_name: unpackerr restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Europe/Copenhagen volumes: - /srv/docker/unpackerr:/config - /data:/data security_opt: - no-new-privileges:trueIn unpackerr.conf, use your server’s LAN IP for Sonarr/Radarr URLs (e.g., http://192.168.1.100:8989) since Unpackerr runs in a separate stack.
16. Connecting Everything Together
Section titled “16. Connecting Everything Together”After deploying all containers, here’s the order to connect them:
Step 1: Verify the VPN
Section titled “Step 1: Verify the VPN”# Check Gluetun is connecteddocker exec gluetun curl -sf https://ipinfo.io# Should show your VPN IP, NOT your real IP
# Verify *arr apps have direct internet (NOT behind VPN)docker exec sonarr curl -sf https://ipinfo.io# Should show your REAL IP
# Check port forwarding (if using PIA/similar)curl -sf http://localhost:8000/v1/openvpn/portforwarded# Should show {"port":12345}Step 2: Configure qBittorrent
Section titled “Step 2: Configure qBittorrent”- Set download paths and categories (section 6)
- If port forwarding is active, the port-sync script updates qBittorrent automatically
Step 3: Set up Prowlarr
Section titled “Step 3: Set up Prowlarr”- Add your torrent indexers
- Connect Prowlarr to Sonarr, Radarr, and Lidarr (section 7)
- Add qBittorrent as download client: host
gluetun, port8090 - Prowlarr auto-syncs indexers to all connected apps
Step 4: Set up the *arr apps
Section titled “Step 4: Set up the *arr apps”For each (Sonarr, Radarr, Lidarr):
- Add root folder (the media library path)
- Add qBittorrent as download client: host
gluetun, port8090 - Note the API key
Step 5: Set up Bazarr
Section titled “Step 5: Set up Bazarr”- Connect to Sonarr (
http://sonarr:8989) and Radarr (http://radarr:7878) using their API keys - Add subtitle providers
Step 6: Set up Plex
Section titled “Step 6: Set up Plex”- Add media libraries pointing to
/data/media/{movies,tv,music}
Step 7: Set up Seerr
Section titled “Step 7: Set up Seerr”- Connect to Plex, Sonarr, and Radarr
Step 8: Test the full flow
Section titled “Step 8: Test the full flow”- Open Seerr → request a movie
- Check Radarr → should show the movie as “searching”
- Check qBittorrent → should show a download starting
- Wait for download → Radarr imports → appears in Plex
- Check Bazarr → subtitles should appear automatically
17. The Complete Docker Compose
Section titled “17. The Complete Docker Compose”Here’s a minimal but complete compose file for the core stack. Copy this, adjust the paths and credentials, and docker compose up -d.
services: # === VPN GATEWAY === gluetun: image: qmcgaw/gluetun:v3 container_name: gluetun restart: unless-stopped cap_add: - NET_ADMIN environment: - VPN_SERVICE_PROVIDER=private internet access - OPENVPN_USER=${OPENVPN_USER} - OPENVPN_PASSWORD=${OPENVPN_PASSWORD} - SERVER_REGIONS=${SERVER_REGIONS} - PORT_FORWARD_ONLY=true - VPN_PORT_FORWARDING=on - UPDATER_PERIOD=24h - TZ=${TZ} volumes: - /srv/docker/gluetun:/gluetun ports: - "8090:8090" # qBittorrent Web UI - "6881:6881" # qBittorrent torrent port - "6881:6881/udp" networks: - vpn - mediastack healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8000/v1/openvpn/status"] interval: 30s timeout: 10s retries: 5
# === DOWNLOAD CLIENT (behind VPN) === qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent restart: unless-stopped network_mode: service:gluetun depends_on: gluetun: condition: service_healthy healthcheck: test: curl -sf http://localhost:8090 || exit 1 interval: 30s timeout: 10s retries: 3 environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - UMASK=${UMASK} - WEBUI_PORT=8090 volumes: - /srv/docker/qbittorrent:/config - /data:/data
# === INDEXER MANAGER === prowlarr: image: lscr.io/linuxserver/prowlarr:latest container_name: prowlarr restart: unless-stopped healthcheck: test: curl -sf http://localhost:9696/ping || exit 1 interval: 30s timeout: 10s retries: 3 environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - /srv/docker/prowlarr/config:/config ports: - "9696:9696" networks: - mediastack
# === TV SHOWS === sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr restart: unless-stopped healthcheck: test: curl -sf http://localhost:8989/ping || exit 1 interval: 30s timeout: 10s retries: 3 environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - UMASK=${UMASK} volumes: - sonarr_config:/config - /data:/data ports: - "8989:8989" networks: - mediastack
# === MOVIES === radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr restart: unless-stopped healthcheck: test: curl -sf http://localhost:7878/ping || exit 1 interval: 30s timeout: 10s retries: 3 environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - UMASK=${UMASK} volumes: - radarr_config:/config - /data:/data ports: - "7878:7878" networks: - mediastack
# === MUSIC === lidarr: image: lscr.io/linuxserver/lidarr:latest container_name: lidarr restart: unless-stopped healthcheck: test: curl -sf http://localhost:8686/ping || exit 1 interval: 30s timeout: 10s retries: 3 environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - UMASK=${UMASK} volumes: - /srv/docker/lidarr/config:/config - /data:/data ports: - "8686:8686" networks: - mediastack
# === SUBTITLES === bazarr: image: lscr.io/linuxserver/bazarr:latest container_name: bazarr restart: unless-stopped healthcheck: test: curl -sf http://localhost:6767/ping || exit 1 interval: 30s timeout: 10s retries: 3 environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - UMASK=${UMASK} volumes: - /srv/docker/bazarr/config:/config - /data:/data ports: - "6767:6767" networks: - mediastack
# === CAPTCHA SOLVER === flaresolverr: image: ghcr.io/flaresolverr/flaresolverr:latest container_name: flaresolverr restart: unless-stopped environment: - TZ=${TZ} ports: - "8191:8191" networks: - mediastack
# === QUALITY PROFILES === recyclarr: image: ghcr.io/recyclarr/recyclarr:latest container_name: recyclarr restart: unless-stopped user: ${PUID}:${PGID} volumes: - /srv/docker/recyclarr:/config networks: - mediastack
# === REAL-TIME GRABBING (optional) === autobrr: image: ghcr.io/autobrr/autobrr:latest container_name: autobrr restart: unless-stopped healthcheck: test: curl -sf http://localhost:7474/ || exit 1 interval: 30s timeout: 10s retries: 3 user: ${PUID}:${PGID} volumes: - /srv/docker/autobrr:/config ports: - "7474:7474" networks: - mediastack
volumes: sonarr_config: driver: local radarr_config: driver: local
networks: vpn: driver: bridge mediastack: driver: bridgeEnvironment file
Section titled “Environment file”PUID=1000PGID=1000TZ=Europe/CopenhagenUMASK=022
# VPN credentialsVPN_SERVICE_PROVIDER=private internet accessOPENVPN_USER=your-vpn-usernameOPENVPN_PASSWORD=your-vpn-passwordSERVER_REGIONS=NetherlandsDeploy
Section titled “Deploy”cd /opt/stacks/mediastackdocker compose up -d
# Watch Gluetun connect to VPNdocker compose logs -f gluetun
# Once connected, verify the split:# qBittorrent should show VPN IPdocker exec qbittorrent curl -sf https://ipinfo.io# Sonarr should show your REAL IPdocker exec sonarr curl -sf https://ipinfo.io18. Healthchecks & Boot Order
Section titled “18. Healthchecks & Boot Order”Without healthchecks, Docker’s depends_on only waits for a container to start — not for the service inside it to actually be ready. This matters because Gluetun needs several seconds to establish the VPN tunnel. If qBittorrent starts before the tunnel is up, it has no internet and errors out.
The boot chain
Section titled “The boot chain”With the split-network topology, the boot order is simpler:
Gluetun starts → VPN tunnel established → healthcheck passes → qBittorrent starts → WebUI responding → healthcheck passes
Meanwhile (in parallel): Sonarr, Radarr, Lidarr, Prowlarr, Bazarr, Autobrr all start immediately on the mediastack network (no VPN dependency)The *arr apps no longer need to wait for the VPN. They start independently and connect to qBittorrent via gluetun:8090 once it’s available. If the VPN is still connecting, the *arr apps work fine — they just can’t send downloads to qBittorrent until it’s ready.
How it works
Section titled “How it works”Gluetun has a built-in healthcheck (/gluetun-entrypoint healthcheck) that only passes once the VPN tunnel is fully established. qBittorrent waits for this:
depends_on: gluetun: condition: service_healthy # Wait for VPN tunnel, not just container startService-level healthchecks let you monitor each container’s actual readiness. The *arr apps all expose a /ping endpoint:
| Service | Healthcheck endpoint | Notes |
|---|---|---|
| Gluetun | Built-in (automatic) | Checks VPN tunnel status |
| qBittorrent | http://localhost:8090 | WebUI availability |
| Prowlarr | http://localhost:9696/ping | |
| Sonarr | http://localhost:8989/ping | |
| Radarr | http://localhost:7878/ping | |
| Lidarr | http://localhost:8686/ping | |
| Bazarr | http://localhost:6767/ping | NOT /api/* (requires auth) |
| Autobrr | http://localhost:7474/ | No /ping — root returns 200 |
Adding a healthcheck
Section titled “Adding a healthcheck”Every service should have one:
sonarr: # ... other config ... healthcheck: test: curl -sf http://localhost:8989/ping || exit 1 interval: 30s timeout: 10s retries: 3curl -sf— silent, fail on HTTP errorsinterval: 30s— check every 30 secondsretries: 3— mark unhealthy after 3 consecutive failures (90 seconds)
Checking health status
Section titled “Checking health status”# See health status of all containersdocker compose ps
# Detailed health log for a specific containerdocker inspect sonarr --format '{{json .State.Health}}' | jq19. First Boot Walkthrough
Section titled “19. First Boot Walkthrough”Here’s the exact order to configure everything after first docker compose up -d:
1. Wait for Gluetun to connect
Section titled “1. Wait for Gluetun to connect”docker compose logs -f gluetun# Wait for "healthy" status2. qBittorrent
Section titled “2. qBittorrent”- Open
http://your-server:8090 - Check logs for temp password:
docker logs qbittorrent 2>&1 | grep "temporary password" - Log in, change password
- Settings → Downloads → Default Save Path:
/data/media_stack/qbittorrent/ - Create categories:
tv,movies,musicwith paths as described in section 6
3. Sonarr
Section titled “3. Sonarr”- Open
http://your-server:8989 - Settings → General → set authentication
- Settings → Media Management → Add Root Folder →
/data/media/tv - Settings → Download Clients → Add → qBittorrent → host
gluetun, port8090, categorytv - Copy API key from Settings → General
4. Radarr
Section titled “4. Radarr”- Open
http://your-server:7878 - Same as Sonarr but root folder
/data/media/movies, categorymovies - Download client: host
gluetun, port8090 - Copy API key
5. Lidarr (if using)
Section titled “5. Lidarr (if using)”- Open
http://your-server:8686 - Root folder
/data/media/music, categorymusic - Download client: host
gluetun, port8090 - Copy API key
6. Prowlarr
Section titled “6. Prowlarr”- Open
http://your-server:9696 - Settings → General → set authentication
- Add indexers (your tracker accounts)
- Settings → Apps → Add Sonarr: Prowlarr URL
http://prowlarr:9696, Sonarr URLhttp://sonarr:8989+ API key - Settings → Apps → Add Radarr: Prowlarr URL
http://prowlarr:9696, Radarr URLhttp://radarr:7878+ API key - Settings → Apps → Add Lidarr: Prowlarr URL
http://prowlarr:9696, Lidarr URLhttp://lidarr:8686+ API key - Settings → Download Clients → Add qBittorrent: host
gluetun, port8090 - Hit “Sync” — indexers now appear in all *arr apps
7. Bazarr
Section titled “7. Bazarr”- Open
http://your-server:6767 - Settings → Sonarr →
http://sonarr:8989+ API key - Settings → Radarr →
http://radarr:7878+ API key - Settings → Providers → add subtitle sources
- Settings → Languages → add your languages
8. Plex
Section titled “8. Plex”- Open
http://your-server:32400/web - Sign in, add libraries:
/data/media/movies,/data/media/tv,/data/media/music
9. Seerr
Section titled “9. Seerr”- Open
http://your-server:5055 - Connect to Plex
- Add Radarr:
http://your-server-LAN-ip:7878+ API key + root folder + quality profile - Add Sonarr:
http://your-server-LAN-ip:8989+ API key + root folder + quality profile
10. Test it
Section titled “10. Test it”Request a movie in Seerr. Watch it flow through Radarr → qBittorrent → Plex. Check Bazarr for subtitles. If it works end-to-end, your mediastack is complete.
20. Troubleshooting
Section titled “20. Troubleshooting”VPN issues
Section titled “VPN issues”| Problem | Fix |
|---|---|
| Gluetun won’t connect | Check VPN credentials. Check docker logs gluetun for errors. Try a different SERVER_REGIONS. |
| ”All connections failed” | Your VPN provider might be down, or the server list is outdated. Delete /srv/docker/gluetun/servers.json and restart. |
| qBittorrent can’t reach the internet | Gluetun isn’t healthy yet. Check docker compose ps — gluetun should show (healthy). |
| Real IP showing in qBittorrent | network_mode: service:gluetun is missing or typo’d. Verify with docker exec qbittorrent curl -sf https://ipinfo.io. |
*Arr app issues
Section titled “*Arr app issues”| Problem | Fix |
|---|---|
| ”Unable to connect to indexer” | Prowlarr indexers not synced. Go to Prowlarr → Settings → Apps → click “Sync”. |
| ”Download client unavailable” | qBittorrent not reachable. Use gluetun:8090 as the host (qBit is behind VPN, reachable via Gluetun on the mediastack network). |
| ”Import failed: destination already exists” | Duplicate file. Check if the file was already imported. May need to clear the download queue. |
| ”Database is locked” (Sonarr/Radarr) | Config is on NFS. Move to a local Docker volume (see section 8). |
| Sonarr/Radarr can’t find downloads | Wrong download path. qBittorrent and the *arr app must see the same /data mount. If one sees /downloads and the other /data, hardlinks break. |
Plex issues
Section titled “Plex issues”| Problem | Fix |
|---|---|
| Plex can’t find media | Library path is wrong. Must match where Radarr/Sonarr puts files: /data/media/movies, /data/media/tv. |
| Claim token expired | Tokens last 4 minutes. Generate a new one at plex.tv/claim, update compose, recreate container. |
| Hardware transcoding not working | Check /dev/dri exists on host (ls -la /dev/dri). Ensure the Plex user has access: sudo usermod -aG video $USER. You need Plex Pass for hardware transcoding. |
Hardlinks not working
Section titled “Hardlinks not working”If files are being copied instead of hardlinked (you see double disk usage):
- Verify all containers mount the same top-level path (
/data:/data) - Verify downloads and media library are on the same filesystem (
df -h /data) - Check with
ls -li— hardlinked files have the same inode number - Cross-device hardlinks are impossible. If your downloads are on a different disk than your library, hardlinks won’t work — consider mergerfs or changing your layout.
Networking cheat sheet
Section titled “Networking cheat sheet”| From → To | URL to use | Why |
|---|---|---|
| Sonarr → qBittorrent | gluetun:8090 | qBit is behind VPN; Gluetun bridges VPN and mediastack networks |
| Prowlarr → Sonarr | sonarr:8989 | Both on mediastack network — Docker DNS resolves container names |
| Bazarr → Sonarr | sonarr:8989 | Same reason |
| Bazarr → Radarr | radarr:7878 | Same reason |
| Recyclarr → Sonarr | sonarr:8989 | Same reason |
| Recyclarr → Radarr | radarr:7878 | Same reason |
| Prowlarr → qBittorrent | gluetun:8090 | Same as Sonarr → qBit |
| Seerr → Sonarr | http://192.168.1.x:8989 | Seerr is on proxy network — must use host IP (port exposed on each service) |
| Cross-Seed → Prowlarr | http://192.168.1.x:9696 | Separate stack on default bridge — must use host IP |
| Unpackerr → Sonarr | http://192.168.1.x:8989 | Separate stack on default bridge — must use host IP |
| Plex → media files | Direct filesystem access | Plex runs on host network, reads files directly |
Glossary
Section titled “Glossary”| Term | Meaning |
|---|---|
| Hardlink | A second filename pointing to the same data on disk. Two paths, one file, one set of disk space. Only works within the same filesystem. |
| Seed/Seeding | Uploading a file to other torrent users after downloading it. Required by most private trackers to maintain a good ratio. |
| Ratio | Upload divided by download. Private trackers often require 1.0+ (upload at least as much as you download). |
| Indexer | A torrent site/tracker that lists available torrents. Prowlarr connects to these. |
| Custom Format | A scoring rule in Sonarr/Radarr that gives points to releases matching criteria (e.g., +10 for Bluray, -50 for CAM). |
| Quality Profile | A set of rules defining which quality levels are acceptable and in what order to prefer them. |
| RSS Feed | How Sonarr/Radarr normally check indexers for new releases (polling every 15-30 min). Autobrr bypasses this with real-time IRC monitoring. |
| Port Forwarding (VPN) | Your VPN provider assigns you a port that external peers can connect to. Without it, you can download but seeding is slow. |
| TRaSH Guides | Community-maintained guides for optimal *arr configuration. The gold standard for quality profiles. |
| network_mode: service:X | Docker feature that makes a container share another container’s network namespace. Used to route qBittorrent through Gluetun. |
| Bridge network | A Docker network that allows containers to communicate by name via DNS. The mediastack network is a bridge network. |
Guide by FalseViking Labs — falsevikinglabs.com