Media Streaming Server

Posted on 28 May 2026

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:

  1. Administrator account: create a strong password and store in KeePassXC

  2. Preferred display language: English (United Kingdom)

  3. 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
  4. 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.