Media Server
Build a self-hosted media center that automatically finds, downloads, organizes, and streams your movies and TV shows. No subscriptions, no content restrictions, no algorithms deciding what you watch.
Goal
Create a media server that:
- Streams your movie and TV library to any device (TV, phone, tablet, browser)
- Automatically monitors for new movies and TV episodes you want
- Searches and downloads content through your configured indexers
- Organizes files with proper naming, metadata, and artwork
- Routes download traffic through VPN for privacy
- Runs entirely self-hosted — no accounts, no cloud
Components
| Component | Catalog Card | Role |
|---|---|---|
| Debian | compute/os | Base operating system. Ubuntu Server also works. |
| Docker | compute/container | Container runtime |
| Jellyfin | applications/media | Media server — stream to all devices |
| Radarr | applications/media | Movie collection manager |
| Sonarr | applications/media | TV series collection manager |
| Prowlarr | applications/media | Indexer manager for Radarr and Sonarr |
| qBittorrent | applications/downloads | Download client |
| WireGuard | network/vpn | VPN for download traffic |
| Caddy | network/proxy | Reverse proxy with HTTPS |
All are A3/T2.
Running Proxmox? Create a VM (Debian, 4+ GB RAM), install Docker inside it, then follow this recipe. Benefit: VM-level snapshots before updates, isolation from other stacks, easy backup via vzdump. See Proxmox VE.
Who is this for?
- Anyone paying for multiple streaming services and tired of content disappearing
- People who already have a media collection and want to stream it properly
- Families who want one place for all their movies and shows
- Anyone who wants automated library management instead of manual downloads
Estimated setup time: 2–3 hours. Minimum hardware: Any x86 machine with 4 GB RAM. Storage depends on your library — plan for 1–4 TB. A dedicated HDD or NAS is recommended for media storage.
Architecture
┌──────────┐
│ Prowlarr │ ← manages indexers
└────┬─────┘
│ syncs indexers to
┌────────┴────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Radarr │ │ Sonarr │ ← monitor for wanted content
│ (movies)│ │ (TV) │
└────┬────┘ └────┬────┘
│ │
└───────┬────────┘
▼ sends downloads to
┌──────────────┐
│ qBittorrent │ ← downloads via VPN
│ (WireGuard) │
└──────┬───────┘
│ completed files
▼
┌──────────┐
│ Jellyfin │ ← streams to devices
└──────────┘
Step-by-Step Instructions
1. Create Project Directory
mkdir -p /opt/media-server && cd /opt/media-server
mkdir -p data/{jellyfin,radarr,sonarr,prowlarr,qbittorrent}
mkdir -p media/{movies,tv}
mkdir -p downloads/{complete,incomplete}
2. Set Up WireGuard VPN
Get a VPN provider that supports WireGuard (Mullvad, IVPN, AirVPN, etc.). Download the WireGuard configuration file.
Create config/wg0.conf with your provider’s settings:
[Interface]
PrivateKey = YOUR_PRIVATE_KEY
Address = 10.x.x.x/32
DNS = 10.x.x.x
[Peer]
PublicKey = PROVIDER_PUBLIC_KEY
AllowedIPs = 0.0.0.0/0
Endpoint = PROVIDER_SERVER:51820
3. Create Docker Compose
cat > docker-compose.yml << 'YAMLEOF'
services:
# ── VPN Container ──────────────────────────────────
# qBittorrent routes ALL traffic through this VPN
wireguard:
image: linuxserver/wireguard
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- TZ=UTC
volumes:
- ./config/wg0.conf:/config/wg_confs/wg0.conf
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
ports:
# qBittorrent WebUI (routed through VPN)
- "8080:8080"
restart: unless-stopped
healthcheck:
test: ["CMD", "ping", "-c", "1", "1.1.1.1"]
interval: 30s
timeout: 5s
retries: 3
# ── Download Client (behind VPN) ───────────────────
qbittorrent:
image: linuxserver/qbittorrent
container_name: qbittorrent
network_mode: "service:wireguard"
environment:
- TZ=UTC
- WEBUI_PORT=8080
volumes:
- ./data/qbittorrent:/config
- ./downloads:/downloads
depends_on:
wireguard:
condition: service_healthy
restart: unless-stopped
# ── Media Server ───────────────────────────────────
jellyfin:
image: jellyfin/jellyfin
container_name: jellyfin
ports:
- "8096:8096"
volumes:
- ./data/jellyfin:/config
- ./media/movies:/data/movies
- ./media/tv:/data/tvshows
environment:
- TZ=UTC
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8096/health"]
interval: 30s
timeout: 5s
retries: 3
# ── Movie Manager ──────────────────────────────────
radarr:
image: linuxserver/radarr
container_name: radarr
ports:
- "7878:7878"
volumes:
- ./data/radarr:/config
- ./media/movies:/movies
- ./downloads:/downloads
environment:
- TZ=UTC
restart: unless-stopped
# ── TV Manager ─────────────────────────────────────
sonarr:
image: linuxserver/sonarr
container_name: sonarr
ports:
- "8989:8989"
volumes:
- ./data/sonarr:/config
- ./media/tv:/tv
- ./downloads:/downloads
environment:
- TZ=UTC
restart: unless-stopped
# ── Indexer Manager ────────────────────────────────
prowlarr:
image: linuxserver/prowlarr
container_name: prowlarr
ports:
- "9696:9696"
volumes:
- ./data/prowlarr:/config
environment:
- TZ=UTC
restart: unless-stopped
# ── Reverse Proxy ──────────────────────────────────
caddy:
image: caddy:latest
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./config/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
restart: unless-stopped
volumes:
caddy_data:
YAMLEOF
4. Create Caddyfile (optional — for domain access)
mkdir -p config
cat > config/Caddyfile << 'EOF'
media.{$DOMAIN} {
reverse_proxy jellyfin:8096
}
EOF
5. Deploy
docker compose up -d
6. Configure the Stack
Order matters. Configure in this sequence:
6a. Prowlarr (:9696)
- Add your indexers (Usenet or torrent indexers)
- Settings → Apps → Add Radarr and Sonarr with their API keys
6b. qBittorrent (:8080)
- Default login: admin / check container logs for generated password
- Settings → Downloads → Set default save path to
/downloads/complete - Verify VPN is working: Tools → Connection → your IP should be the VPN IP
6c. Radarr (:7878)
- Settings → Media Management → Add root folder:
/movies - Settings → Download Clients → Add qBittorrent (host:
wireguard, port: 8080) - Movies → Add New → Search for a movie to test
6d. Sonarr (:8989)
- Settings → Media Management → Add root folder:
/tv - Settings → Download Clients → Add qBittorrent (host:
wireguard, port: 8080) - Series → Add New → Search for a show to test
6e. Jellyfin (:8096)
- Complete the setup wizard
- Add library: Movies →
/data/movies - Add library: TV Shows →
/data/tvshows - Install mobile apps (iOS/Android) and point to your server
7. Verify VPN Protection
Critical: Ensure download traffic goes through VPN.
# Check qBittorrent's external IP (should be VPN, not your real IP)
docker exec wireguard curl -s ifconfig.me
If this shows your real IP, the VPN is not working. Do not proceed until fixed.
Failure Modes
| Component | Failure scenario | Impact | Recovery |
|---|---|---|---|
| WireGuard | VPN disconnects | qBittorrent loses internet (by design — kill switch). Downloads pause. | Check VPN config, restart: docker compose restart wireguard |
| qBittorrent | Crash | Downloads pause. Radarr/Sonarr queue items. | docker compose restart qbittorrent. Incomplete downloads resume. |
| Radarr/Sonarr | Database corruption | Collection metadata lost. Media files untouched. | Restore from backup. Movies/shows still playable in Jellyfin. |
| Prowlarr | Indexer sync fails | New searches fail. Existing library unaffected. | Check indexer status. Re-add indexers if needed. |
| Jellyfin | Database corruption | Library metadata lost. Rescan rebuilds from media files. | Delete Jellyfin DB, rescan. All media files are intact. |
| Caddy | Config error | Remote access lost | Access via LAN IP:PORT while fixing. |
| Disk full | Storage exhausted | Downloads fail, Jellyfin may stop indexing | Clear completed downloads, expand storage, prune old content. |
Blast radius: WireGuard failure is the most impactful — it stops all downloads immediately (kill switch by design). But this is a feature, not a bug. Jellyfin continues streaming existing content regardless of any other component failure.
Quick recovery:
# Restart a single service
docker compose restart <service-name>
# Check VPN status
docker exec wireguard wg show
# Full stack restart
docker compose down && docker compose up -d
What Replaces What
| Need | You used to use | Now you use | Autonomy gain |
|---|---|---|---|
| Streaming | Netflix / Disney+ / HBO | Jellyfin | A0 → A3 |
| Finding movies | Browse streaming apps | Radarr | Manual → Automated |
| Finding TV shows | Browse streaming apps | Sonarr | Manual → Automated |
| Downloading | Manual search | qBittorrent + Prowlarr | Manual → Automated |
| Privacy | ISP sees everything | WireGuard VPN | Exposed → Protected |
Maintenance
Weekly:
- Check qBittorrent — clear completed downloads
- Check disk usage:
du -sh media/* downloads/*
Monthly:
- Update containers:
docker compose pull && docker compose up -d - Check Radarr/Sonarr activity — clean stuck queue items
- Verify VPN:
docker exec wireguard curl -s ifconfig.me
As needed:
- Add new movies to Radarr watchlist
- Add new TV shows to Sonarr
- Update indexer credentials in Prowlarr
Cost
| Item | One-time | Monthly |
|---|---|---|
| Server (mini PC or NAS) | $150–300 | — |
| Storage (4 TB HDD) | $80–120 | — |
| VPN service (Mullvad/IVPN) | — | $5/month |
| Electricity | — | ~$5/month |
| Total | $230–420 | ~$10/month |
Compare: Netflix ($15) + Disney+ ($14) + HBO Max ($16) + Hulu ($18) = $63/month = $756/year. Hardware pays for itself in 4–6 months.