Media Streaming Server
The source material covers Plex Media Server. The self-hosting landscape for media streaming has shifted significantly since the source was written, and it shifted materially again in April 2025.
The Plex situation
Since April 2025, remote playback of personal video from a Plex Media Server requires either the server admin to have an active Plex Pass subscription, or each viewer to purchase a Remote Watch Pass. Streaming personal video from your own server, to yourself, when not on the same local network, now requires a paid subscription or per-user purchase.
This is a meaningful change to the proposition of self-hosting with Plex. The data stays on your hardware, but access to that data from outside your home now has a monetary gate that Plex can raise or change again in the future.
For a network running a WireGuard VPN that makes every device appear to be on the internal network, this restriction may not affect day-to-day use. But it is an architectural dependency on a commercial service for access to data you own and host yourself. That sits badly with the philosophy of this entire series.
Jellyfin
Jellyfin is the recommendation for this series. It is an open source, completely free media server with no subscription requirements, no account needed, no telemetry, and no remote access restrictions. It is a hard fork of Emby, created when Emby moved to a closed-source model in 2018.
Jellyfin handles movies, TV shows, music, photos, books, and live TV (with a tuner). Client apps are available for every platform: Android, iOS, Apple TV, Roku, Android TV, Fire TV, web browser, and desktop. The web interface works from any browser without any app.
The desktop section of this series already uses Jellyfin for local media playback via Haruna and the desktop Jellyfin client. This page covers the server side.
Container setup
Clone the base template:
pct clone 100 142 --hostname media --full
pct start 142
Inside the container:
hostnamectl set-hostname media.yourdomain.net
sed -i 's/base-template/media/g' /etc/hosts
Resize the container disk for Jellyfin’s metadata and transcoding cache:
# From the Proxmox host
pct resize 142 rootfs 20G
Media storage
Media files live on the NAS, mounted via NFS into the container:
sudo apt install -y nfs-common
Add to /etc/fstab:
nas.yourdomain.net:/volume1/media /var/lib/jellyfin/media nfs defaults,_netdev,nofail,ro 0 0
Mount as read-only: Jellyfin reads the library but should not modify it. Create the mount point and mount:
sudo mkdir -p /var/lib/jellyfin/media
sudo mount -a
Create the directory structure on the NAS (do this from the desktop or NAS management interface):
/volume1/media/
├── films/
├── tv/
├── music/
└── photos/
Installation
Add the Jellyfin repository:
curl -fsSL https://repo.jellyfin.org/install-debuntu.sh | sudo bash
This script adds the Jellyfin apt repository and installs Jellyfin. Verify after installation:
jellyfin --version
sudo systemctl status jellyfin
GPU transcoding passthrough (optional)
The February server has a GTX 1080 (once the HBA situation is resolved). Jellyfin can use the GPU for hardware-accelerated transcoding, which significantly reduces CPU load during video playback.
For LXC containers, GPU passthrough requires configuration on the Proxmox host. Add the GPU device to the container:
# From the Proxmox host
# Find the GPU device nodes
ls /dev/nvidia*
# Add GPU passthrough to the container (CT ID 142)
pct set 142 --dev0 /dev/nvidia0
pct set 142 --dev1 /dev/nvidiactl
pct set 142 --dev2 /dev/nvidia-modeset
Inside the container, install the NVIDIA container runtime:
sudo apt install -y nvidia-container-toolkit
Configure Jellyfin to use NVIDIA hardware acceleration: Dashboard > Playback > Transcoding > Hardware Acceleration > NVIDIA NVENC.
GPU transcoding is covered in more depth in the Proxmox GPU passthrough section. For initial setup, CPU transcoding is sufficient.
Initial configuration
Navigate to the Jellyfin web interface:
http://10.1.0.x:8096
The setup wizard runs on first access. Configure:
Administrator account: create a strong password and store in KeePassXC
Preferred display language: English (United Kingdom)
Media libraries: add each media type pointing at the NFS mount subdirectories:
- Films →
/var/lib/jellyfin/media/films - TV →
/var/lib/jellyfin/media/tv - Music →
/var/lib/jellyfin/media/music
- Films →
Metadata language: English (United Kingdom)
After the wizard, Jellyfin scans the library and downloads metadata (artwork, descriptions, cast information) automatically.
nginx configuration
Create /etc/nginx/sites-available/media.yourdomain.net:
server {
listen 80;
listen [::]:80;
server_name media.yourdomain.net;
location /.well-known/acme-challenge/ {
root /var/www/acme-challenge;
allow all;
}
location / {
return 301 https://media.yourdomain.net$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name media.yourdomain.net;
ssl_certificate /etc/letsencrypt/live/media.yourdomain.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.yourdomain.net/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/media.yourdomain.net/chain.pem;
ssl_session_cache shared:media:10m;
include snippets/security-headers.conf;
include snippets/deny-sensitive-files.conf;
include snippets/error-pages.conf;
# Jellyfin CSP
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' wss://media.yourdomain.net; media-src 'self' blob:; frame-ancestors 'self'; form-action 'self'; worker-src 'self' blob:; upgrade-insecure-requests;" always;
# Large buffer for video streaming
client_max_body_size 20M;
proxy_buffering off;
location / {
proxy_pass http://10.1.0.x:8096;
include snippets/proxy-headers.conf;
# Required for Jellyfin streaming
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
access_log /var/log/nginx/media.access.log main;
error_log /var/log/nginx/media.error.log;
}
Activate:
sudo ln -s /etc/nginx/sites-available/media.yourdomain.net \
/etc/nginx/sites-enabled/media.yourdomain.net
sudo nginx -t && sudo systemctl reload nginx
Firewall rules
Jellyfin does not need any ports open directly to the internet. Access from outside the network goes via the WireGuard VPN, which makes the device appear to be on the internal network. The nginx HTTPS port forward covers web browser access from external networks.
For DLNA discovery (allows media players on the local network to find Jellyfin automatically), UDP port 1900 needs to be accessible on the local network:
sudo ufw allow from 10.0.0.0/8 to any port 1900 proto udp
API key for desktop integration
The Haruna media player and the desktop Jellyfin client configured in the desktop section connect to this server. Generate an API key in Jellyfin:
Dashboard > API Keys > New API Key
Store the key in KeePassXC. Configure Haruna and the Jellyfin desktop client with:
- Server URL:
https://media.yourdomain.net - API Key: the generated key
- Username / Password: the admin account
Multiple user accounts
Jellyfin supports multiple user accounts with different library access and parental controls. Create accounts for family members via Dashboard > Users > New User. Each user has their own watch history, continue watching queue, and preferences.
Unlike Plex, creating users costs nothing and has no per-user restrictions.
Backups
Jellyfin’s configuration and metadata database live in /etc/jellyfin/ and /var/lib/jellyfin/. Add these to borgmatic:
source_directories:
- /etc/jellyfin
- /var/lib/jellyfin/data
- /var/lib/jellyfin/config
Exclude the transcoding cache and media directory from backups:
exclude_patterns:
- /var/lib/jellyfin/transcodes
- /var/lib/jellyfin/media
The media files themselves are on the NAS and covered by the NAS backup.
Why not Emby
Emby is Jellyfin’s predecessor. It became closed-source in 2018 and introduced a subscription model for features like hardware transcoding, multiple users, and mobile sync. Jellyfin was forked specifically to keep a free, open alternative available. The server side of Emby now requires Emby Premier for features that Jellyfin provides without restriction.
Why not Plex
Covered at the top of this page. The April 2025 remote playback change is the primary reason. For a network where the WireGuard VPN makes remote access transparent, the practical impact is limited today. The principle is the problem: a subscription gate on access to your own data, which Plex can change again whenever it chooses.
Jellyfin’s hardware transcoding support has matured significantly since the Emby fork. If the February server’s GTX 1080 is available to the container via GPU passthrough, NVENC hardware transcoding eliminates most CPU load from simultaneous streams. Without hardware transcoding, the Ryzen 7 5700X handles multiple concurrent software transcodes comfortably.