Anonymous Streaming Guide
Build your own private media streaming setup with self-hosted open source solutions
Last verified: October 2026
Self-Hosted Media Stack: October 2026
Jellyfin 12.2 (the project dropped the old "10.x" numbering with 12.0 in September 2026), Sonarr v4, Radarr v6, Prowlarr v2 and qBittorrent 5.2 give you a Netflix-style experience you fully control. Downloads run inside a gluetun VPN container so nothing leaks outside the tunnel.
Why Self-Hosted Streaming?
Self-hosting gives you complete control over your media library without the restrictions, tracking, or subscription fees of commercial platforms. Own your content, protect your privacy, and access everything from anywhere.
Complete Privacy
No viewing history sent to anyone
Total Control
Your content, your rules
Cost Effective
Hardware once, no monthly fees
Legal note: This stack is for media you have the rights to: your own rips, public domain and Creative Commons works, and content licensed for download. A VPN hides traffic from your ISP; it does not make copyright infringement legal.
Essential Media Stack Components
Jellyfin 12.2
Web UI port 8096
Free, open-source media server. No account with a company, no telemetry, hardware transcoding included free (unlike Plex Pass).
Sonarr v4
Web UI port 8989
Watches for new TV episodes, grabs them through your download client, then renames and files them into your library.
Radarr v6
Web UI port 7878
The same automation for movies: wanted list, quality profiles, custom formats and automatic upgrades.
Prowlarr v2
Web UI port 9696
One place to manage indexers. It pushes them to Sonarr and Radarr automatically so you never configure them twice.
Infrastructure & Hardware Setup
1Hardware Requirements & Recommendations
Basic Setup (1-2 Users)
- CPU: Intel N100/N150 mini PC or similar
- RAM: 8 GB
- Storage: 500 GB SSD + 4 TB HDD
- Network: Gigabit Ethernet
- Power: ~10-15W idle
Recommended (5-10 Users)
- CPU: Intel Core i5 (Quick Sync) or AMD Ryzen 5
- RAM: 16 GB
- Storage: 1 TB NVMe + 8-16 TB HDD
- GPU: Intel iGPU handles most transcodes
- Power: ~30-60W typical
Enthusiast (10+ Users)
- CPU: Core i7/i9 or Ryzen 7/9
- RAM: 32 GB+
- Storage: 2 TB NVMe + multi-drive array
- GPU: Intel Arc or NVIDIA for many 4K transcodes
- Power: 100W+ under load
Storage Planning:
- ⢠SSD: OS, Docker, app configs and databases
- ⢠SSD: Jellyfin metadata and transcode cache
- ⢠HDD: the whole /data tree (torrents + media)
- ⢠Keep torrents and media on the same filesystem so hardlinks work
2Operating System Selection
Recommended: Ubuntu Server 26.04 LTS
- ⢠"Resolute Raccoon", released April 23, 2026
- ⢠Standard support until May 2031 (ESM to 2036)
- ⢠First-class Docker support and documentation
- ⢠Recent kernel for new Intel/AMD iGPU transcoding
Alternative: Debian 13 "Trixie"
- ⢠Rock-solid stability, minimal footprint
- ⢠No Snap packages or vendor extras
- ⢠Excellent for headless servers
- ⢠Same Docker install steps as Ubuntu
3Initial Server Configuration
# Update system packages
sudo apt update && sudo apt full-upgrade -y
# Essential tools
sudo apt install -y curl git htop tree unzip unattended-upgrades
# Timezone (list options with: timedatectl list-timezones)
sudo timedatectl set-timezone America/New_York
# Firewall: allow SSH BEFORE enabling or you lock yourself out
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
# Automatic security updates
sudo dpkg-reconfigure -plow unattended-upgradesSSH Hardening:
- ⢠Copy your key first:
ssh-copy-id user@server - ⢠Set
PasswordAuthentication no - ⢠Set
PermitRootLogin no - ⢠Restart:
sudo systemctl restart ssh
Docker vs UFW:
Ports published by Docker bypass UFW rules. Only publish what you need, keep the server behind your router (no port forwards), and use the remote access options below instead of opening ports.
4Storage & Directory Layout
Use a single /data tree (the layout popularized by the TRaSH Guides). When downloads and library share one filesystem and one mount, Sonarr and Radarr can hardlink files: instant imports, no duplicate disk usage, and you keep seeding.
# One shared /data tree so hardlinks and instant moves work
sudo mkdir -p /data/torrents/{movies,tv,music}
sudo mkdir -p /data/media/{movies,tv,music}
# App configs live separately from media
sudo mkdir -p /opt/mediaserver/config/{jellyfin,sonarr,radarr,prowlarr,qbittorrent,gluetun}
# Owned by your media user (check your IDs with: id)
sudo chown -R $USER:$USER /data /opt/mediaserver
sudo chmod -R a=,a+rX,u+w,g+w /data/data
āāā torrents/ <- qBittorrent downloads here
ā āāā movies/
ā āāā tv/
ā āāā music/
āāā media/ <- Sonarr/Radarr hardlink finished files here
āāā movies/
āāā tv/
āāā music/
/opt/mediaserver
āāā compose.yaml
āāā .env
āāā config/
āāā jellyfin/ sonarr/ radarr/
āāā prowlarr/ qbittorrent/ gluetun/Mounting Data Drives:
- ⢠Format with ext4 or XFS (or ZFS/Btrfs if you want snapshots)
- ⢠Mount by UUID in
/etc/fstab(find it withlsblk -f) - ⢠Pool multiple drives with mergerfs or a RAID/ZFS array, and keep it mounted at /data
- ⢠RAID is not a backup: keep irreplaceable files somewhere else too
Docker Container Deployment
1Docker Engine & Compose Installation
# Remove distro-packaged Docker if present
for pkg in docker.io docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y $pkg
done
# Install Docker Engine + Compose plugin from Docker's script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Run docker without sudo (log out and back in afterwards)
sudo usermod -aG docker $USER
# Verify: Engine 29.x and Compose v5.x as of October 2026
docker --version
docker compose version
docker run --rm hello-worldCompose ships as a Docker plugin. Always type docker compose (with a space). The old standalone docker-compose binary is retired.
2Environment Variables
# /opt/mediaserver/.env (chmod 600, never commit it)
PUID=1000
PGID=1000
TZ=America/New_York
CONFIG_ROOT=/opt/mediaserver/config
DATA_ROOT=/data
# VPN (from your provider's WireGuard config generator)
VPN_SERVICE_PROVIDER=protonvpn
WIREGUARD_PRIVATE_KEY=replace-with-your-private-key
SERVER_COUNTRIES=SwitzerlandGenerate the WireGuard key in your VPN provider's account dashboard. Protect the file with chmod 600 .env since it holds your VPN credentials.
3Complete compose.yaml
Save as /opt/mediaserver/compose.yaml. qBittorrent has no ports of its own: it lives inside gluetun's network, so if the VPN drops, it has no route to the internet at all.
services:
gluetun:
image: qmcgaw/gluetun:v3.41.3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "8080:8080" # qBittorrent Web UI (published here, not on qbittorrent)
volumes:
- ${CONFIG_ROOT}/gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
- VPN_PORT_FORWARDING=on # Proton VPN / PIA only
- TZ=${TZ}
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun" # all traffic goes through the VPN tunnel
depends_on:
gluetun:
condition: service_healthy
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- WEBUI_PORT=8080
volumes:
- ${CONFIG_ROOT}/qbittorrent:/config
- ${DATA_ROOT}/torrents:/data/torrents
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ${CONFIG_ROOT}/prowlarr:/config
ports:
- "9696:9696"
restart: unless-stopped
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ${CONFIG_ROOT}/sonarr:/config
- ${DATA_ROOT}:/data
ports:
- "8989:8989"
restart: unless-stopped
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ${CONFIG_ROOT}/radarr:/config
- ${DATA_ROOT}:/data
ports:
- "7878:7878"
restart: unless-stopped
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: ${PUID}:${PGID}
volumes:
- ${CONFIG_ROOT}/jellyfin:/config
- ${DATA_ROOT}/media:/data/media:ro
devices:
- /dev/dri:/dev/dri # Intel/AMD hardware transcoding (remove if unused)
ports:
- "8096:8096"
restart: unless-stoppedChange VPN_SERVICE_PROVIDER to match your provider (gluetun supports dozens, including mullvad, protonvpn, private internet access and airvpn). Remove VPN_PORT_FORWARDING if your provider does not support it.
4Deploy & Monitor the Stack
cd /opt/mediaserver
docker compose up -d
# Status and logs
docker compose ps
docker compose logs -f gluetun qbittorrentWeb UIs (on your LAN):
- Jellyfin http://server:8096
- qBittorrent http://server:8080
- Prowlarr http://server:9696
- Sonarr http://server:8989
- Radarr http://server:7878
First Logins:
- ⢠Jellyfin: setup wizard, add libraries at /data/media/movies and /data/media/tv
- ⢠*arr apps: set Authentication to Forms and create a user
- ⢠qBittorrent: temporary password is in
docker logs qbittorrent
Download Client Configuration
1qBittorrent First-Run Setup
- 1. Open
http://server:8080, log in withadminand the temporary password from the logs - 2. Options > Web UI: set a strong username and password immediately
- 3. Options > Downloads: confirm the default save path is
/data/torrents - 4. Run the leak test in the VPN section below before adding any torrents
2Categories & Paths for Sonarr/Radarr
# Categories (right-click Categories > Add category)
movies -> /data/torrents/movies
tv -> /data/torrents/tv
music -> /data/torrents/music
# Options > Downloads
Default Torrent Management Mode: Automatic
Default Save Path: /data/torrents
When Category Save Path changed: Relocate torrentAutomatic mode lets Sonarr and Radarr assign a category and have qBittorrent place the files correctly. Do not enable "move completed" style options; the *arr apps import from the category folder and hardlink into /data/media.
3Privacy & Connection Settings
Connection:
- UPnP / NAT-PMP: Disabled (gluetun handles ports)
- Listening port: your VPN's forwarded port, if you have one
- Global max connections: 200-500
- Upload limit: set to roughly 80% of your upstream
BitTorrent Privacy:
- Encryption: Require encryption (fewer peers, less ISP visibility)
- Anonymous mode: Enable (hides client details)
- DHT / PeX / LSD: Disable for private trackers; public torrents need DHT
- Remember: peers always see your VPN IP. That's how BitTorrent works
4Without Docker: Bind to the VPN Interface
Running qBittorrent directly on a desktop with a VPN app instead? Bind it to the VPN interface so it cannot fall back to your real connection.
# Find the VPN interface name
ip -brief link
# Typical names
wg0 # WireGuard (most providers in 2026)
tun0 # OpenVPN (where still offered)
proton0 # Proton VPN app on Linux- 1. Options > Advanced > Network Interface: pick the VPN interface (not "Any interface")
- 2. Optional IP address to bind to: All addresses
- 3. Test: start a legal test torrent, disconnect the VPN, confirm transfers drop to zero
Indexers & Prowlarr Configuration
1Secure Prowlarr
- 1. Open
http://server:9696 - 2. Settings > General > Authentication: Forms (login page), Required: Enabled
- 3. Create a username and a strong password
2Add Indexers
- 1. Indexers > Add Indexer, search for the trackers or Usenet indexers you have accounts with
- 2. Enter API keys or credentials, then click Test
- 3. Use Tags to limit specific indexers to specific apps if needed
- 4. Prefer indexers that publish legal content you are entitled to download
3Sync Indexers to Sonarr & Radarr
- 1. In Sonarr: Settings > General, copy the API Key (do the same in Radarr)
- 2. In Prowlarr: Settings > Apps > + > Sonarr
- 3. Prowlarr Server:
http://prowlarr:9696, Sonarr Server:http://sonarr:8989, paste the API key - 4. Sync Level: Full Sync, Test, Save. Repeat for Radarr with
http://radarr:7878 - 5. Sonarr/Radarr > Settings > Download Clients > qBittorrent: host
gluetun, port8080, categorytv/movies
Containers on the same compose network reach each other by service name. qBittorrent is reached through gluetun because it shares that container's network.
4Optional: FlareSolverr
Some sites sit behind Cloudflare challenges. FlareSolverr runs a headless browser to solve them. Only add it if an indexer actually needs it.
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
ports:
- "8191:8191"
restart: unless-stoppedThen in Prowlarr: Settings > Indexers > + > FlareSolverr, host http://flaresolverr:8191, add a tag, and apply that tag only to indexers that need it.
Request Management with Seerr
Overseerr and Jellyseerr merged into a single project, Seerr, in February 2026. It supports Jellyfin, Emby and Plex, and on first start it migrates existing Overseerr/Jellyseerr configs automatically. New installs should use Seerr.
1Install Seerr
- 1. Follow the Docker Compose instructions at
docs.seerr.devfor the current image name and port - 2. Add the service to your compose.yaml with a config volume under
${CONFIG_ROOT}/seerr - 3.
docker compose up -dand open the web UI
2Connect Jellyfin, Sonarr & Radarr
- 1. Sign in with your Jellyfin admin account (choose Jellyfin as the media server)
- 2. Jellyfin URL:
http://jellyfin:8096, then select which libraries to sync - 3. Settings > Services: add Radarr (
http://radarr:7878) and Sonarr (http://sonarr:8989) with their API keys - 4. Pick default quality profiles and root folders (
/data/media/movies,/data/media/tv)
3Users & Permissions
- ⢠Import Jellyfin users so family members log in with their existing accounts
- ⢠Grant "Request" but not "Auto-Approve" to most users
- ⢠Set request limits per user (for example 10 movies per week)
4Approvals & Notifications
- ⢠Requests land in a queue; approving one sends it straight to Sonarr or Radarr
- ⢠Notifications: email, ntfy, Gotify, Discord, Telegram and webhooks are built in
- ⢠For privacy, prefer a self-hosted push service like ntfy or Gotify
VPN Integration & Privacy
1Why Gluetun
- ⢠One container holds the VPN tunnel plus a built-in firewall that blocks everything outside it
- ⢠Any container attached with
network_mode: "service:gluetun"inherits the kill switch - ⢠DNS goes through the tunnel by default, so your ISP never sees lookups
- ⢠Supports WireGuard and OpenVPN across many providers (current release: v3.41.3)
2Choosing a Provider (October 2026)
Port forwarding available:
- ⢠Proton VPN (paid plans, P2P servers)
- ⢠Private Internet Access
No port forwarding:
- ⢠Mullvad (removed 2023; WireGuard-only since Jan 2026)
- ⢠IVPN (removed 2023)
Without a forwarded port you can still download, but you connect to fewer peers and seed poorly. Avoid free VPNs for torrenting entirely.
3Port Forwarding Setup
# Proton VPN / PIA: gluetun requests a forwarded port automatically.
# Read the current port:
docker exec gluetun cat /tmp/gluetun/forwarded_port
# Then set qBittorrent > Options > Connection > Listening Port to that number.
# The port can change on reconnect; the gluetun wiki documents an
# up-command hook that pushes it into qBittorrent's API automatically.
# Providers that assign you a fixed forwarded port instead:
# - FIREWALL_VPN_INPUT_PORTS=<your port> (opens it on the tunnel)4Leak Testing & Kill Switch Verification
# Gluetun's built-in health check (should print "healthy")
docker inspect --format '{{.State.Health.Status}}' gluetun
# Public IP seen through the tunnel (must be the VPN's IP, not yours)
docker exec gluetun wget -qO- https://ipinfo.io/ip
# qBittorrent shares gluetun's network stack, so this matches the line above.
# Kill switch test: stop the VPN and confirm qBittorrent loses all connectivity
docker stop gluetun
docker exec qbittorrent curl -s --max-time 5 https://ipinfo.io/ip || echo "blocked: kill switch works"
docker start gluetun && docker restart qbittorrentIf the IP check ever shows your home IP, stop qBittorrent immediately and check that it has network_mode: "service:gluetun" and no ports: section of its own.
Remote Access & Security
1Best Option: Private VPN Mesh (No Open Ports)
Reach Jellyfin from anywhere without exposing anything to the internet. Tailscale is the easiest; Headscale is a self-hosted control server for Tailscale clients; plain WireGuard (for example wg-easy) works if you can forward one UDP port on your router.
# Install Tailscale on the server
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Install the Tailscale app on your phone/laptop, sign in to the same tailnet,
# then open Jellyfin at http://<server-tailscale-name>:8096
tailscale status2If You Must Expose Jellyfin: Reverse Proxy + HTTPS
# /etc/caddy/Caddyfile (Caddy fetches and renews HTTPS certs automatically)
jellyfin.example.com {
reverse_proxy localhost:8096
}- ⢠Caddy gets Let's Encrypt certificates automatically; Traefik and Nginx Proxy Manager are alternatives
- ⢠Forward only 443 on your router to the proxy
- ⢠Expose Jellyfin only. Never expose qBittorrent, Prowlarr, Sonarr or Radarr
- ⢠Your home IP becomes visible to anyone who looks up the domain
3Brute-Force Protection
- ⢠CrowdSec or fail2ban watching the reverse proxy and Jellyfin logs
- ⢠Jellyfin: Dashboard > Users, disable the admin account's remote access, set login attempt limits
- ⢠Unique, long passwords for every user
4Domain & DNS
- ⢠Use a subdomain you control; enable WHOIS privacy at the registrar
- ⢠Dynamic home IP? Run a DDNS updater container for your DNS provider
- ⢠For the server's own lookups, Quad9 (9.9.9.9) validates DNSSEC on all endpoints since June 2026. Mullvad's public DNS shuts down November 2, 2026
Mobile Apps & Clients
1Official Jellyfin Clients
- ⢠Android: Jellyfin (Play Store, F-Droid)
- ⢠iOS / iPadOS: Jellyfin Mobile, or Swiftfin (native app from the Jellyfin project)
- ⢠Android TV / Fire TV: Jellyfin for Android TV
- ⢠Roku, LG webOS, Samsung Tizen: official apps (check jellyfin.org/downloads)
- ⢠Desktop: any browser, or the official desktop client
- ⢠Kodi: Jellyfin add-on
2Popular Third-Party Clients
- ⢠Findroid (Android): open source, native UI, offline downloads
- ⢠Infuse (Apple devices): polished player, paid subscription for some features
3Connecting Clients
- 1. At home:
http://server-ip:8096 - 2. Away: Tailscale address (Step 1 above) or your HTTPS domain
- 3. Use Quick Connect (Dashboard > General) to sign in TVs without typing passwords
4Offline Downloads & Casting
- ⢠Enable "Allow media downloading" per user in Dashboard > Users
- ⢠Jellyfin's Android app supports Chromecast; Apple clients support AirPlay
- ⢠Direct Play (no transcoding) saves CPU: pick clients that support your file formats
Monitoring & Maintenance
1Uptime Monitoring
Uptime Kuma checks every service and alerts you (ntfy, email, Telegram and more) when something goes down. Add it to compose.yaml:
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
volumes:
- ${CONFIG_ROOT}/uptime-kuma:/app/data
ports:
- "3001:3001"
restart: unless-stopped2Updating Safely
cd /opt/mediaserver
# Pull new images and recreate only containers that changed
docker compose pull
docker compose up -d
# Clean up old image layers
docker image prune -fThe original Watchtower auto-updater was archived in December 2025. Update manually on a schedule (read release notes first, especially for Jellyfin major versions), or use a notify-only tool such as Diun so you know when images change.
3Backups
# Nightly config backup (media is too big; back it up separately or re-acquire)
cd /opt/mediaserver
docker compose stop
tar -czf ~/backups/mediaserver-config-$(date +%F).tar.gz config compose.yaml .env
docker compose start
# Keep the 14 most recent archives
ls -1t ~/backups/mediaserver-config-*.tar.gz | tail -n +15 | xargs -r rmSonarr, Radarr and Prowlarr also make their own scheduled backups (System > Backup). Copy archives off the server, ideally encrypted (restic or Borg).
4Troubleshooting Checklist
- ⢠Imports fail: paths differ between containers. Every app must see the same
/datapath - ⢠Files copied, not hardlinked: torrents and media are on different filesystems or mounts
- ⢠qBittorrent unreachable: gluetun is unhealthy. Check
docker compose logs gluetun - ⢠Buffering: check Jellyfin Dashboard > Activity for transcode reasons and enable hardware acceleration
Self-Hosted vs Commercial Streaming
Self-Hosted Advantages
- No monthly subscription fees
- Complete content control and ownership
- No tracking or data collection
- Quality limited only by your storage
Considerations
- Initial setup complexity
- Hardware and electricity costs
- Maintenance and updates required
- You are responsible for your own security