Most of the time a media server just sends the file. Sometimes the app on the other end cannot play it: a 4K HEVC film on an old TV, a high-bitrate file on a phone on mobile data, subtitles the app cannot draw itself. Then the server converts the video while you watch. That is transcoding, and on the CPU it is slow, hot and often not fast enough for real time. An Intel integrated GPU has dedicated video engines for exactly this, and the Jellyfin documentation recommends QSV (Intel Quick Sync Video) on mainstream Intel GPUs. Getting it to work inside a Proxmox container is a short list of steps. Knowing that it works is a separate job, and the one most guides skip.
The trap: a dropdown that says QSV
Set Hardware acceleration to Intel QuickSync in the Jellyfin dashboard and the page looks done. It is not. If the container cannot open the render device, or the group number is wrong, or the source codec is not ticked for hardware decoding, Jellyfin quietly falls back to software. The film still plays. The CPU does the work. Nothing in the player tells you.
The fix is a ladder: six checks, each ruling out one failure, climbed in order. If a rung fails, fix it before climbing further.
Two things first. Intel processors whose model ends in "F" have no integrated GPU, and the Jellyfin docs say not to use them for this. And the old recipe of a privileged container with lxc.cgroup2.devices.allow lines is kept in the Jellyfin docs only for Proxmox VE 7 and older. On Proxmox VE 8 and 9 the container stays unprivileged.
Rung 1: the host has a render device
On the Proxmox host:
ls -l /dev/dri
getent group render
You should see renderD128 (and card0 or similar) owned by the render or video group. If /dev/dri is missing, the host kernel has no driver for the GPU, and nothing below will work until it does.
Rung 2: the device reaches the container with the right group
Proxmox VE 8 and newer can pass a device into a container through the web interface, and the Jellyfin documentation recommends exactly this route. The detail that matters is the group id, and it is the container's group id, not the host's. Inside the container:
# inside the media container (CT 110)
getent group render
# render:x:993: <- your number may differ
On some systems the group is video or input instead of render; use whichever exists. Then on the host, in the container's Resources tab, choose Add > Device Passthrough, enter /dev/dri/renderD128 as the device path, and set the GID in the advanced options to the number you just found. The same thing from the command line, using the dev[n] option from the Proxmox documentation:
# on the Proxmox host
pct set 110 --dev0 /dev/dri/renderD128,gid=993
pct reboot 110
After the restart, ls -l /dev/dri inside the container should show renderD128 owned by the render group. The container is still unprivileged and the host keeps using the GPU. One catch from the Jellyfin docs: you must be logged in to Proxmox as root to add device passthrough; if the option is unavailable, that is why.
If Jellyfin runs in a VM instead, the whole GPU moves into it with PCI passthrough, and the host, and every other guest, lose it.
Rung 3: Docker hands the device to Jellyfin
The official Jellyfin image already contains Intel's user-mode media drivers and the OpenCL runtime, so nothing needs installing inside it. Docker only has to pass the device through and add the group that owns it, with the number from rung 2:
# /opt/stacks/jellyfin/compose.yaml (GPU part)
services:
jellyfin:
image: jellyfin/jellyfin:12.2
user: 1000:1000
group_add:
- "993" # gid of render inside the LXC
devices:
- /dev/dri/renderD128:/dev/dri/renderD128
If you have two GPUs, the second one is usually renderD129.
Rung 4: the drivers open the GPU from inside
Before touching any Jellyfin setting, confirm the container can actually use the device. Both commands come from the Jellyfin documentation:
docker exec -it jellyfin /usr/lib/jellyfin-ffmpeg/vainfo
docker exec -it jellyfin /usr/lib/jellyfin-ffmpeg/ffmpeg -v verbose \
-init_hw_device vaapi=va -init_hw_device opencl@va
vainfo should name the Intel iHD driver and list the supported profiles and entry points. The second command should list an Intel OpenCL device. If you get failed to initialize display or a permission error, the group in group_add does not match the group that owns /dev/dri/renderD128 inside the container. Fix the number and recreate the container.
Rung 5: the dashboard settings match your GPU
Now the transcoding settings in the Jellyfin dashboard:
- Hardware acceleration: Intel QuickSync (QSV), or VA-API if you chose that.
- Device: leave
/dev/dri/renderD128unless you use a second GPU. - Hardware decoding: tick only the codecs your GPU supports, as the docs instruct. The
vainfooutput and the codec tables in the Jellyfin Intel guide tell you which. Older iGPUs cannot decode HEVC 10-bit or AV1 in hardware; AV1 decoding starts with Gen 12 (11th Gen Core) graphics. - Enable hardware encoding.
- Turn on tone mapping, so HDR sources look right on SDR screens. Both tone-mapping methods need a GPU with HEVC 10-bit decoding.
Rung 6: watch the GPU do the work
A setting that says "QSV" proves nothing. Make Jellyfin transcode and watch the GPU.
- Play a film in the web client and force a transcode by picking a lower quality in the player settings.
- In the dashboard, the active session should show that it is transcoding, not direct playing.
- On the machine that owns the GPU (the Proxmox host for a container, the VM for a VM), install and run
intel_gpu_top:
apt update && apt install -y intel-gpu-tools
intel_gpu_top
The Jellyfin guide explains the engine rows: Video is the QSV decoder or encoder at work, VideoEnhance is QSV VPP processing, Render/3D covers OpenCL workloads such as tone mapping. During a hardware transcode the Video row should be busy. If every row sits at zero while the CPU climbs in top, the transcode is running in software: go back down the ladder.
When it still uses the CPU
- Subtitles burned in. Image-based subtitles force a burn-in. It is still a video transcode, so make sure it is accelerated, and prefer text subtitles the client can render itself.
- Codec not ticked. Jellyfin then decodes in software even when encoding on the GPU.
- Wrong device after a reboot. With more than one GPU, render node numbers can change. Pin the right one in both the passthrough and the compose file.
Checklist
- CPU model does not end in "F"; Quick Sync confirmed on Intel's spec page.
-
/dev/dri/renderD128exists on the host. - Render group gid read inside the container; device passed with that gid; container still unprivileged.
- Jellyfin compose file has the device under
devicesand the gid undergroup_add. -
vainfoshows the iHD driver; the OpenCL check finds an Intel device. - QSV selected with only supported codecs ticked; hardware encoding and tone mapping on.
- A forced transcode shows the Video engine busy in
intel_gpu_top. - After a host kernel update,
vainfoand a forced transcode checked again.
Once this works, most of your playback should not need it at all: direct play, where the server just sends the file, is the goal, and transcoding is the safety net underneath it.
Originally published on the Homelab the Right Way blog. This article is based on the ebook Stream Your Own Library; every book in the series has a free sample. Written with AI help and edited by me.
Top comments (0)