Skip to content

The Complete Mediastack Guide

A mediastack is a collection of self-hosted applications that work together to:

  1. Find media (movies, TV shows, music) on torrent indexers
  2. Download it through a VPN-protected torrent client
  3. Organize it into a clean library with proper naming
  4. Serve it to your devices via a media server (like Plex or Jellyfin)
  5. Fetch subtitles automatically
  6. 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.

AppRoleOne-sentence summary
GluetunVPN containerRuns a VPN tunnel that the torrent client routes traffic through
qBittorrentDownload clientDownloads torrents through the VPN
ProwlarrIndexer managerConnects to torrent sites and shares them with Sonarr/Radarr/Lidarr
SonarrTV show managerMonitors TV shows, searches for new episodes, sends them to qBittorrent
RadarrMovie managerSame as Sonarr but for movies
LidarrMusic managerSame as Sonarr but for music albums
BazarrSubtitle managerAutomatically downloads subtitles for everything Sonarr and Radarr grab
PlexMedia serverServes your library to TVs, phones, and browsers
SeerrRequest managerPretty UI where users can browse and request movies/shows
RecyclarrQuality syncAutomatically applies community-recommended quality profiles
FlareSolverrCAPTCHA solverHelps Prowlarr access indexers that use Cloudflare protection

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 Interstellar

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.

  • 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.
  • Docker Engine and Docker Compose v2 installed
  • Linux host (Debian, Ubuntu, or similar). This guide assumes Linux.
  • 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.

Every container needs to run with the same user and group IDs so file permissions work. Find yours:

Terminal window
id
# Output: uid=1000(youruser) gid=1000(youruser) groups=...

Note your uid (PUID) and gid (PGID). You’ll use these in every container.

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.

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.

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.flac
Terminal window
# Create the directory tree
# Adjust the base path to wherever your storage is
sudo mkdir -p /data/media_stack/qbittorrent/{movies,tv,music}
sudo mkdir -p /data/media/{movies,tv,music}
# Set ownership to your PUID:PGID
sudo chown -R 1000:1000 /data

Every container mounts the same /data directory:

volumes:
- /path/to/your/storage:/data # Same mount in EVERY container
ContainerWhat it uses inside /data
qBittorrentDownloads to /data/media_stack/qbittorrent/{movies,tv,music}
SonarrSees downloads at /data/media_stack/qbittorrent/tv, library at /data/media/tv
RadarrSees downloads at /data/media_stack/qbittorrent/movies, library at /data/media/movies
LidarrSees downloads at /data/media_stack/qbittorrent/music, library at /data/media/music
PlexReads 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.

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.

  • 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

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

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.

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:8090

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 container
GLUETUN_API="http://localhost:8000"
QBIT_API="http://localhost:8090"
QBIT_USER="admin"
QBIT_PASS="your-qbit-password"
# Get forwarded port from Gluetun
PORT=$(curl -sf "${GLUETUN_API}/v1/openvpn/portforwarded" | jq -r '.port')
if [ -z "$PORT" ] || [ "$PORT" = "0" ]; then
echo "No forwarded port available"
exit 1
fi
# Log into qBittorrent
COOKIE=$(curl -sf -c - "${QBIT_API}/api/v2/auth/login" \
--data "username=${QBIT_USER}&password=${QBIT_PASS}" | grep SID | awk '{print $NF}')
# Set the listening port
curl -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).

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.

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 mount

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

  1. Open http://your-server:8090
  2. Default login: check the container logs for the temporary password (docker logs qbittorrent)
  3. Change the password immediately in Settings → Web UI
  4. Go to Settings → Downloads: - Default Save Path: /data/media_stack/qbittorrent/
  5. Go to Settings → BitTorrent: - Enable DHT, PeX, and Local Peer Discovery (unless your private trackers forbid it) - Set global upload/download limits if needed

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.

AppCategorySave Path
Sonarrtv/data/media_stack/qbittorrent/tv
Radarrmovies/data/media_stack/qbittorrent/movies
Lidarrmusic/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.

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.

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:
- mediastack
  1. Open http://your-server:9696
  2. Set up authentication (Settings → General → Authentication)
  3. Add indexers (Indexers → Add Indexer): - Search for your tracker name - Enter your credentials/API key/cookie - Test the connection
  4. Connect Prowlarr to your *arr apps (Settings → Apps → Add Application): - Add Sonarr: Prowlarr URL http://prowlarr:9696, Sonarr URL http://sonarr:8989, API key from Sonarr - Add Radarr: Prowlarr URL http://prowlarr:9696, Radarr URL http://radarr:7878, API key from Radarr - Add Lidarr: Prowlarr URL http://prowlarr:9696, Lidarr URL http://lidarr:8686, API key from Lidarr
  5. Add download client: Settings → Download Clients → qBittorrent - Host: gluetun (NOT localhost — qBittorrent is behind the VPN, reachable via Gluetun on the mediastack network) - Port: 8090

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:
- mediastack

In Prowlarr: Settings → Indexers → Add → FlareSolverr → URL: http://flaresolverr:8191

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.

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:
- mediastack
  1. Open http://your-server:8989
  2. Set authentication: Settings → General → Authentication → Forms
  3. Add root folder: Settings → Media Management → Root Folders → Add/data/media/tv
  4. 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
  5. Note your API key: Settings → General → API Key (you’ll need this for Prowlarr and Bazarr)
  6. Add a show: Series → Add New → search and add

Sonarr renames files on import using a configurable pattern. The default is fine for most people:

Breaking Bad - S01E01 - Pilot.mkv

You can customize this in Settings → Media Management → Episode Naming.

  1. qBittorrent downloads an episode to /data/media_stack/qbittorrent/tv/
  2. Sonarr detects the completed download (it polls qBittorrent every 60 seconds)
  3. Sonarr creates a hardlink from the download to /data/media/tv/Show Name/Season XX/
  4. The file now exists at both paths but uses disk space only once
  5. The original stays in the download folder for seeding
  6. When seeding is done (or after a configurable delay), Sonarr can optionally delete the original

Radarr is Sonarr but for movies. The setup is nearly identical.

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:
- mediastack
  1. Open http://your-server:7878
  2. Set authentication: Settings → General → Authentication → Forms
  3. Add root folder: Settings → Media Management → Root Folders → Add/data/media/movies
  4. Add download client: Settings → Download Clients → Add → qBittorrent - Host: gluetun, Port: 8090, Category: movies
  5. Note your API key for Prowlarr and Bazarr
  6. Add a movie: Movies → Add New

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.

Lidarr manages your music library. Same pattern as Sonarr/Radarr.

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:
- mediastack
  1. Open http://your-server:8686
  2. Set authentication
  3. Add root folder: /data/media/music
  4. Add download client: qBittorrent on gluetun:8090, category: music
  5. Add an artist: Artist → Add New

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.

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:
- mediastack
  1. Open http://your-server:6767
  2. Connect to Sonarr: Settings → Sonarr → URL: http://sonarr:8989, API key
  3. Connect to Radarr: Settings → Radarr → URL: http://radarr:7878, API key
  4. Add subtitle providers: Settings → Providers - OpenSubtitles.com — free account, decent coverage - Addic7ed — good for TV shows - Gestdown — alternative
  5. Set languages: Settings → Languages → add your preferred languages
  6. 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.

Plex serves your media library to your devices. It runs on the host network for best performance and DLNA support.

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)
  • 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.
  1. Open http://your-server:32400/web
  2. Sign in with your Plex account
  3. Add libraries: - Movies: /data/media/movies - TV Shows: /data/media/tv - Music: /data/media/music
  4. Under each library’s advanced settings, enable “Use original title” and set the scanner/agent to your preference

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

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)
  1. Open http://your-server:5055
  2. Sign in with your Plex account
  3. Connect to Plex: enter your server URL
  4. Connect to Radarr: URL http://your-server-ip:7878, API key, root folder /data/media/movies
  5. 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.

recyclarr:
image: ghcr.io/recyclarr/recyclarr:latest
container_name: recyclarr
restart: unless-stopped
user: 1000:1000
volumes:
- /srv/docker/recyclarr:/config
networks:
- mediastack
  1. First run generates a config template at /srv/docker/recyclarr/recyclarr.yml
  2. 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:
# ...
  1. Run it manually once to verify: docker exec recyclarr recyclarr sync
  2. After that, it runs automatically on a daily schedule

These are optional but useful tools that improve the stack.

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:
- mediastack

Setup: http://your-server:7474 → connect to qBittorrent (host: gluetun, port: 8090), add your tracker IRC channels, create filters.

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:
- mediastack

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

Terminal window
# Separate stack — /opt/stacks/cross-seed/compose.yaml
cross-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).

Some trackers upload releases as RAR archives. Unpackerr watches for completed downloads and automatically extracts them so Sonarr/Radarr can import the media files.

Terminal window
# Separate stack — /opt/stacks/unpackerr/compose.yaml
unpackerr:
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:true

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

After deploying all containers, here’s the order to connect them:

Terminal window
# Check Gluetun is connected
docker 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}
  • Set download paths and categories (section 6)
  • If port forwarding is active, the port-sync script updates qBittorrent automatically
  • Add your torrent indexers
  • Connect Prowlarr to Sonarr, Radarr, and Lidarr (section 7)
  • Add qBittorrent as download client: host gluetun, port 8090
  • Prowlarr auto-syncs indexers to all connected apps

For each (Sonarr, Radarr, Lidarr):

  1. Add root folder (the media library path)
  2. Add qBittorrent as download client: host gluetun, port 8090
  3. Note the API key
  • Connect to Sonarr (http://sonarr:8989) and Radarr (http://radarr:7878) using their API keys
  • Add subtitle providers
  • Add media libraries pointing to /data/media/{movies,tv,music}
  • Connect to Plex, Sonarr, and Radarr
  1. Open Seerr → request a movie
  2. Check Radarr → should show the movie as “searching”
  3. Check qBittorrent → should show a download starting
  4. Wait for download → Radarr imports → appears in Plex
  5. Check Bazarr → subtitles should appear automatically

Here’s a minimal but complete compose file for the core stack. Copy this, adjust the paths and credentials, and docker compose up -d.

/opt/stacks/mediastack/compose.yaml
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: bridge
/opt/stacks/mediastack/.env
PUID=1000
PGID=1000
TZ=Europe/Copenhagen
UMASK=022
# VPN credentials
VPN_SERVICE_PROVIDER=private internet access
OPENVPN_USER=your-vpn-username
OPENVPN_PASSWORD=your-vpn-password
SERVER_REGIONS=Netherlands
Terminal window
cd /opt/stacks/mediastack
docker compose up -d
# Watch Gluetun connect to VPN
docker compose logs -f gluetun
# Once connected, verify the split:
# qBittorrent should show VPN IP
docker exec qbittorrent curl -sf https://ipinfo.io
# Sonarr should show your REAL IP
docker exec sonarr curl -sf https://ipinfo.io

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.

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.

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 start

Service-level healthchecks let you monitor each container’s actual readiness. The *arr apps all expose a /ping endpoint:

ServiceHealthcheck endpointNotes
GluetunBuilt-in (automatic)Checks VPN tunnel status
qBittorrenthttp://localhost:8090WebUI availability
Prowlarrhttp://localhost:9696/ping
Sonarrhttp://localhost:8989/ping
Radarrhttp://localhost:7878/ping
Lidarrhttp://localhost:8686/ping
Bazarrhttp://localhost:6767/pingNOT /api/* (requires auth)
Autobrrhttp://localhost:7474/No /ping — root returns 200

Every service should have one:

sonarr:
# ... other config ...
healthcheck:
test: curl -sf http://localhost:8989/ping || exit 1
interval: 30s
timeout: 10s
retries: 3
  • curl -sf — silent, fail on HTTP errors
  • interval: 30s — check every 30 seconds
  • retries: 3 — mark unhealthy after 3 consecutive failures (90 seconds)
Terminal window
# See health status of all containers
docker compose ps
# Detailed health log for a specific container
docker inspect sonarr --format '{{json .State.Health}}' | jq

Here’s the exact order to configure everything after first docker compose up -d:

Terminal window
docker compose logs -f gluetun
# Wait for "healthy" status
  • 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, music with paths as described in section 6
  • 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, port 8090, category tv
  • Copy API key from Settings → General
  • Open http://your-server:7878
  • Same as Sonarr but root folder /data/media/movies, category movies
  • Download client: host gluetun, port 8090
  • Copy API key
  • Open http://your-server:8686
  • Root folder /data/media/music, category music
  • Download client: host gluetun, port 8090
  • Copy API key
  • Open http://your-server:9696
  • Settings → General → set authentication
  • Add indexers (your tracker accounts)
  • Settings → Apps → Add Sonarr: Prowlarr URL http://prowlarr:9696, Sonarr URL http://sonarr:8989 + API key
  • Settings → Apps → Add Radarr: Prowlarr URL http://prowlarr:9696, Radarr URL http://radarr:7878 + API key
  • Settings → Apps → Add Lidarr: Prowlarr URL http://prowlarr:9696, Lidarr URL http://lidarr:8686 + API key
  • Settings → Download Clients → Add qBittorrent: host gluetun, port 8090
  • Hit “Sync” — indexers now appear in all *arr apps
  • 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
  • Open http://your-server:32400/web
  • Sign in, add libraries: /data/media/movies, /data/media/tv, /data/media/music
  • 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

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.

ProblemFix
Gluetun won’t connectCheck 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 internetGluetun isn’t healthy yet. Check docker compose ps — gluetun should show (healthy).
Real IP showing in qBittorrentnetwork_mode: service:gluetun is missing or typo’d. Verify with docker exec qbittorrent curl -sf https://ipinfo.io.
ProblemFix
”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 downloadsWrong download path. qBittorrent and the *arr app must see the same /data mount. If one sees /downloads and the other /data, hardlinks break.
ProblemFix
Plex can’t find mediaLibrary path is wrong. Must match where Radarr/Sonarr puts files: /data/media/movies, /data/media/tv.
Claim token expiredTokens last 4 minutes. Generate a new one at plex.tv/claim, update compose, recreate container.
Hardware transcoding not workingCheck /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.

If files are being copied instead of hardlinked (you see double disk usage):

  1. Verify all containers mount the same top-level path (/data:/data)
  2. Verify downloads and media library are on the same filesystem (df -h /data)
  3. Check with ls -li — hardlinked files have the same inode number
  4. 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.
From → ToURL to useWhy
Sonarr → qBittorrentgluetun:8090qBit is behind VPN; Gluetun bridges VPN and mediastack networks
Prowlarr → Sonarrsonarr:8989Both on mediastack network — Docker DNS resolves container names
Bazarr → Sonarrsonarr:8989Same reason
Bazarr → Radarrradarr:7878Same reason
Recyclarr → Sonarrsonarr:8989Same reason
Recyclarr → Radarrradarr:7878Same reason
Prowlarr → qBittorrentgluetun:8090Same as Sonarr → qBit
Seerr → Sonarrhttp://192.168.1.x:8989Seerr is on proxy network — must use host IP (port exposed on each service)
Cross-Seed → Prowlarrhttp://192.168.1.x:9696Separate stack on default bridge — must use host IP
Unpackerr → Sonarrhttp://192.168.1.x:8989Separate stack on default bridge — must use host IP
Plex → media filesDirect filesystem accessPlex runs on host network, reads files directly
TermMeaning
HardlinkA 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/SeedingUploading a file to other torrent users after downloading it. Required by most private trackers to maintain a good ratio.
RatioUpload divided by download. Private trackers often require 1.0+ (upload at least as much as you download).
IndexerA torrent site/tracker that lists available torrents. Prowlarr connects to these.
Custom FormatA scoring rule in Sonarr/Radarr that gives points to releases matching criteria (e.g., +10 for Bluray, -50 for CAM).
Quality ProfileA set of rules defining which quality levels are acceptable and in what order to prefer them.
RSS FeedHow 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 GuidesCommunity-maintained guides for optimal *arr configuration. The gold standard for quality profiles.
network_mode: service:XDocker feature that makes a container share another container’s network namespace. Used to route qBittorrent through Gluetun.
Bridge networkA Docker network that allows containers to communicate by name via DNS. The mediastack network is a bridge network.

Guide by FalseViking Labs — falsevikinglabs.com