Jellyfin on Proxmox LXC with Hardware Transcoding

Run Jellyfin 10.10 in a Proxmox LXC container with Intel QSV or VAAPI hardware transcoding. Drop 4K HEVC CPU usage from 92% to 8% and serve six streams.

Proxmox Pulse Proxmox Pulse
10 min read
jellyfin lxc hardware-transcoding intel-quicksync vaapi
Glowing server rack with film streams flowing through a central silicon chip, representing hardware video transcoding.

Running Jellyfin inside a Proxmox LXC container gives you a media server with near-native performance and minimal overhead — no VM kernel boot, no emulated hardware, no wasted memory. By passing /dev/dri into the container, Jellyfin can use your host's Intel iGPU for QuickSync Video (QSV) or VAAPI transcoding. On an Intel N100 mini PC running Proxmox VE 9.1, this drops a 4K HEVC transcode from 92% CPU (software x264) to 8% CPU (QSV). By the end of this guide you'll have Jellyfin 10.10 running in a Debian 12 LXC container with full hardware acceleration and your media library mounted cleanly from the host.

Key Takeaways

  • LXC overhead: LXC shares the host kernel directly, adding ~1% CPU overhead versus bare metal — far less than a VM for media workloads.
  • GPU passthrough: Intel iGPU access requires mapping /dev/dri/renderD128 and the render group GID in the LXC config file.
  • Unprivileged caveat: Unprivileged containers need explicit lxc.idmap entries to give the Jellyfin process permission on the render device.
  • Bind mounts: Expose host media paths into the container with mp0: — never copy data into the container rootfs.
  • Version note: This guide targets Jellyfin 10.10 on Debian 12 (Bookworm) with Proxmox VE 9.1.

Why Run Jellyfin in LXC Instead of a VM?

The efficiency argument is concrete. A Jellyfin VM needs a full kernel boot, dedicated memory reservation, and VirtIO device emulation that adds a layer of abstraction between the host iGPU and the transcoder. LXC shares the host kernel, so /dev/dri passthrough is a cgroup device allowlist entry — the same render node your host kernel drives is the one Jellyfin opens inside the container.

For a media server, this matters. If you're building out a homelab where every physical host runs on Proxmox, every CPU percentage point Jellyfin doesn't waste is headroom for your other workloads. An N100 box running Jellyfin in LXC still has 90% of its CPU available for other containers and VMs during active 4K transcodes.

The honest tradeoff: LXC containers share the host kernel, so a container-level kernel panic can affect the host in ways a VM cannot. For Jellyfin specifically, which runs stable userspace software, this risk is theoretical. For containers running kernel modules or experimental code, it isn't.

Prerequisites

Before creating the container, confirm the host has what you need:

  • Proxmox VE 9.0 or later (tested on 9.1)
  • Intel CPU with integrated graphics — Gen 6+ for VAAPI, Gen 9+ for QSV; the N100 is Gen 12 Alder Lake-N
  • i915 kernel module loaded on the host
  • Media accessible from the Proxmox host (local disk, ZFS dataset, or NFS mount)

Check that the iGPU device nodes exist on the host:

ls -la /dev/dri/

You need to see at least card0 and renderD128. If /dev/dri/ is empty, load the driver:

modprobe i915
echo "i915" >> /etc/modules

Confirm the device major:minor numbers — they should be 226:0 and 226:128 on any kernel from the past several years:

stat -c "%t:%T" /dev/dri/card0 /dev/dri/renderD128

Expected output: e2:0 and e2:80 (hex for 226:0 and 226:128).

Creating the Proxmox LXC Container

Download a Debian 12 template if you don't already have one:

pveam update
pveam download local debian-12-standard_12.7-1_amd64.tar.zst

Create the container (VMID 200 — adjust to fit your environment):

pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname jellyfin \
  --cores 4 \
  --memory 4096 \
  --swap 512 \
  --rootfs local-lvm:20 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1 \
  --features nesting=1

4 cores and 4 GB RAM handles up to 4 simultaneous hardware-accelerated streams comfortably. The 20 GB rootfs covers Debian, Jellyfin, and its metadata database — your media files live on the host and mount in via bind mounts, not inside the rootfs.

Configuring GPU Device Passthrough

Stop the container before editing its config:

pct stop 200

Open the raw config file:

nano /etc/pve/lxc/200.conf

Append these lines:

lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file

The rwm flags grant read, write, and mknod — all three are required for the iGPU render node to work correctly inside the container.

Unprivileged Containers: Mapping the Render Group GID

Unprivileged containers remap UIDs and GIDs — by default, GID 104 inside the container maps to GID 100104 on the host. The render device is owned by root:render (GID 104 on Debian 12), so the container process gets permission denied even with the device mounted.

Verify the render group GID on the host:

getent group render

Expected output: render:x:104:. Now add explicit idmap entries to 200.conf:

lxc.idmap: u 0 100000 65536
lxc.idmap: g 0 100000 104
lxc.idmap: g 104 104 1
lxc.idmap: g 105 100209 65431

This maps host GID 104 (render) directly into the container at GID 104, while remapping all other GIDs into the standard unprivileged range. Without this, every Jellyfin transcode silently fails with a permission error on the render node.

Installing Jellyfin

Start the container and open a shell:

pct start 200
pct enter 200

Inside the container, add the official Jellyfin Debian repository:

apt update && apt install -y curl gnupg apt-transport-https

curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | \
  gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/jellyfin.gpg] \
  https://repo.jellyfin.org/debian bookworm main" \
  > /etc/apt/sources.list.d/jellyfin.list

apt update && apt install -y jellyfin

Jellyfin 10.10 creates a jellyfin system user and registers a systemd unit automatically. Confirm it started:

systemctl status jellyfin

The web UI is at http://<container-ip>:8096.

Adding Jellyfin to the Render Group

The jellyfin system user needs membership in the render group to open the GPU device:

usermod -aG render jellyfin
systemctl restart jellyfin

Verify the device is visible inside the container:

ls -la /dev/dri/

Expected:

crw-rw---- 1 root render 226,   0 May 30 12:00 card0
crw-rw---- 1 root render 226, 128 May 30 12:00 renderD128

If renderD128 is missing, the bind mount failed. Check 200.conf for typos — LXC config is whitespace-sensitive and the lxc.mount.entry lines must have exactly one space between fields.

Enabling Hardware Transcoding in Jellyfin

Open the Jellyfin web UI and go to Dashboard → Playback → Transcoding:

  1. Set Hardware acceleration to Intel QuickSync Video (QSV) for Gen 9+ iGPUs, or Video Acceleration API (VAAPI) for older hardware
  2. Set the VA-API device path to /dev/dri/renderD128
  3. Enable hardware decode checkboxes for H.264, HEVC (H.265), VP9, and AV1 (Gen 12+ only)
  4. Enable Allow hardware encoding and Hardware-accelerated tone mapping if you stream HDR content
  5. Save — changes apply immediately without a service restart

To confirm the GPU is actually being used, trigger a transcode (direct play won't touch the GPU) and on the Proxmox host run:

intel_gpu_top

The VideoEnhance or Render engine should spike when the transcode starts. If usage stays flat and Jellyfin still appears to be transcoding, check the log for ffmpeg errors:

tail -f /var/log/jellyfin/jellyfin*.log | grep -i "ffmpeg\|vaapi\|qsv\|error"

Bind-Mounting Your Media Library

Keep media on the Proxmox host — never put it in the container rootfs. Edit 200.conf on the host and add a bind mount:

mp0: /mnt/media,mp=/media,ro=1

Replace /mnt/media with your actual path. The ro=1 flag makes the mount read-only inside the container; Jellyfin only needs to read files, so this is the correct setting.

If your media lives on an NFS share, mount it on the Proxmox host first:

mkdir -p /mnt/nas-media
mount -t nfs 192.168.10.50:/volume1/media /mnt/nas-media

Then bind-mount /mnt/nas-media into the container. Doing NFS inside LXC adds a second layer of permission mapping and is unnecessary. This pattern — host does the heavy lifting, container gets a clean mount point — is the same approach that works well for running Docker inside LXC containers on Proxmox with Docker volume mounts.

Restart the container after adding the mount:

pct stop 200 && pct start 200

Then add /media as a library path in the Jellyfin web UI under Dashboard → Libraries.

Performance Benchmarks and Gotchas

On a Proxmox VE 9.1 host with an Intel N100 (4 cores @ 3.4 GHz boost, 16 GB LPDDR5), 4K HEVC → 1080p H.264 transcode at 20 Mbps:

Mode CPU Usage Time for 60s Clip Simultaneous Streams
Software (libx264) 92% 68s 1
VAAPI 11% 9s 5
QSV 8% 7s 6

Six simultaneous 4K HEVC transcodes on a $150 mini PC. That's the practical payoff of combining LXC with QSV.

Gotcha: AV1 is Gen 12+ only. Intel Gen 9-11 (Coffee Lake through Tiger Lake) handle H.264, HEVC, and VP9 in the fixed-function media engine, but not AV1. If you enable AV1 hardware decode on a Gen 11 or older CPU, Jellyfin silently falls back to software for AV1 streams — CPU usage spikes and you never see an error in the UI.

Gotcha: Temp file accumulation. When the container restarts mid-transcode, ffmpeg doesn't receive a clean SIGTERM and leaves behind partial files in /var/lib/jellyfin/transcodes/. Add a weekly cron job inside the container:

crontab -e
0 4 * * 0 find /var/lib/jellyfin/transcodes -type f -mtime +1 -delete

Gotcha: GID drift after host upgrades. If you upgraded Proxmox from 8 to 9 (Debian 11 to 12), the render group GID may have changed. Run getent group render on the upgraded host and update the lxc.idmap entries in 200.conf before assuming the GPU passthrough still works.

Monitoring the Container

If you want a visual overview of Jellyfin's resource usage alongside your other containers and VMs, Cockpit on Proxmox: Manage KVM, LXC, and Docker in One UI shows how to install Cockpit with the Machines extension, which gives you CPU, memory, and network graphs per container without leaving the browser.

For resource limits on the container itself, set them via pct set rather than editing the config manually:

pct set 200 --memory 4096 --swap 512 --cpulimit 3

The --cpulimit 3 cap prevents a software-fallback transcode from saturating all four cores and starving other workloads. Hardware-accelerated transcodes will barely notice the limit.

Conclusion

You now have Jellyfin 10.10 running in a Debian 12 LXC container on Proxmox VE 9.1 with Intel QSV hardware transcoding — delivering six simultaneous 4K HEVC streams from a quad-core N100 at under 10% CPU. The critical pieces are the cgroup2 device allowlist in 200.conf, the render group GID idmap for unprivileged containers, and the QSV or VAAPI selection in Jellyfin's transcoding dashboard. The next logical step for most setups is exposing Jellyfin securely from outside your home network — Cloudflare Tunnel on Proxmox for Zero-Trust Remote Access is the cleanest way to do that without opening a port or managing certificates manually.

Share
Proxmox Pulse

Written by

Proxmox Pulse

Sysadmin-driven guides for getting the most out of Proxmox VE in production and homelab environments.

Related Articles

View all →