Guide
Jellyfin hardware transcoding in an unprivileged Proxmox LXC with Intel Quick Sync
Since Proxmox VE 8.1 a container can get the iGPU's render node from one config line, owned by the group you name. The cgroup2 lines most guides still teach were written for Proxmox VE 7, and they need a privileged container or a patch on the host to work.
How do you give Jellyfin Intel Quick Sync in an unprivileged Proxmox LXC?
On Proxmox VE 8.1 or later, add dev0: /dev/dri/renderD128,gid=993 to the container, where 993 is the render group's number inside the container (getent group render there). Proxmox makes its own copy of the device node, gives it to that group through the container's id map, and adds the cgroup2 allow rule itself, so you add no lxc.cgroup2 lines and no 100000. Then add the jellyfin user to render inside the container and restart. The GUI has the same option from 8.2, under Resources, Add, Device Passthrough.
On Proxmox VE 8.1 or later, pass the render node with one line and drop the old cgroup2 lines: dev0: /dev/dri/renderD128,gid=993. Here 993 is the number of the render group inside the container. Proxmox makes its own copy of the device node, gives it to that group, and adds the cgroup2 rule for you. You do not add 100000 to anything. Then add the jellyfin user to render inside the container, restart, and test with vainfo as that user.
The old route still works, but it was written for Proxmox VE 7. It hands the container the host's own device node, with the host's owner. In an unprivileged container that owner has no number, so the node shows as nobody:nogroup and Jellyfin cannot open it. Every "add 100000 to the render GID" step in forum guides exists to patch that.
The terms this answer turns on#
Hardware transcoding means the GPU converts the video instead of the CPU. Intel's engine for it is Quick Sync. Jellyfin reaches it through two interfaces, QSV and VA-API, and its docs call QSV "preferred on mainstream GPUs" on Linux. Both need the same thing: a process that is allowed to open the GPU's render node.
The render node is the device file /dev/dri/renderD128. A device file is how Linux lets a program talk to hardware. It has an owner, a group and a mode like any file. It also has two numbers that name the device, and for the first render node they are 226 and 128, the "226:128" in every guide. On a Debian or Ubuntu host, Jellyfin's docs show the node as crw-rw---- owned by root and render, so only root and members of render can open it.
The render GID is the number of that group. It is not fixed: Jellyfin's docs use 989 as an example, and the host and the container can each give render a different number.
An unprivileged container is one whose user and group ids are shifted on the host. Proxmox VE's default adds 100000, so container gid 993 is host gid 100993. A host id outside 100000 to 165535 has no number in the container. It shows as 65534, which ls prints as nobody or nogroup. The full arithmetic is in Why an unprivileged LXC gets permission denied on a bind mount.
The cgroup2 device rule is a second gate. Even with the right owner, the kernel refuses a container any device that is not on its allow list. lxc.cgroup2.devices.allow: c 226:128 rwm puts the render node on that list.
Two routes circulate, and one was written for Proxmox VE 7#
Jellyfin's own Intel page gives both. Under "Proxmox VE 8 or Newer" it says to set up a Device Passthrough in the web interface and set the GID. Under "Proxmox VE 7 or Older" it gives three lines for /etc/pve/lxc/<id>.conf:
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=fileThe same section says that for this route "Jellyfin needs to run in a privileged LXC container". That note explains the rest. The mount entry is a bind mount of the host's own node, owner and all. On the host, that owner is root and the host's render group. Neither is mapped in an unprivileged container, so the node shows as 65534:65534, and Jellyfin falls into "other", which has no rights at all.
The forum guide that ranks for this search keeps the container unprivileged and patches the owner. It adds a fourth line, lxc.hook.pre-start: sh -c "chown 100000:100154 /dev/dri/renderD128", where 154 was that author's render GID inside the container. The LXC manual says a pre-start hook runs "in the host's namespace". So that chown changes the host's node. From then on the host's render group no longer owns its own render node.
The dev option does the job without touching the host's node. Proxmox's release notes date it: the dev0, dev1 options arrived in Proxmox VE 8.1, released 23 November 2023, for the API and CLI only. The GUI followed in 8.2, on 24 April 2024. So Jellyfin's "8 or newer" heading is slightly loose: on 8.0 you have neither.
If you are stuck below 8.1 and must stay unprivileged, map the group instead of using the hook. An lxc.idmap that passes the container's render GID to the host's render GID leaves the host node alone. The bind mount guide shows how to write that full map, with its /etc/subgid line.
What dev0 does, read from Proxmox's source#
The pct.conf(5) manual lists the option as dev[n]: [[path=]<Path>] [,deny-write=<1|0>] [,gid=<integer>] [,mode=<Octal access mode>] [,uid=<integer>]. It describes gid as the "Group ID to be assigned to the device node". The pve-container source shows what that means in practice.
Before the container starts, the prestart hook mounts a small tmpfs at /var/lib/lxc/<id>/passthrough. For each dev entry, create_passthrough_device_node in LXC.pm makes a new device node there. It carries the same device numbers as the host's node. Its mode is 0660 unless you set mode=, and its owner is uid and gid, both 0 by default.
Then both ids go through map_ct_uid_to_host and map_ct_gid_to_host, which apply the container's own id map. So gid=993 becomes host gid 100993 under the default map. A container with custom lxc.idmap lines gets its own map applied the same way. Last, the autodev hook bind-mounts that copy onto /dev/dri/renderD128 inside the container.
The same file writes the cgroup rule: lxc.cgroup2.devices.allow = c 226:128 rw. With deny-write=1 it allows reading only. The host's own /dev/dri/renderD128 is never chowned or bind-mounted.
Two more details come from the source. Setting a dev entry is refused for anyone but root@pam. That matches Jellyfin's note that other administrator accounts cannot do it. And the mapping function returns any id outside the container's range unchanged. So gid=100993, written from the old "add 100000" habit, happens to land on the same host owner. It works by accident, and the config then says something it does not mean. Write 993.
A worked case: container 105#
Take container 105, an unprivileged Debian 12 container running Jellyfin from Jellyfin's own repository. Suppose getent group render inside it prints render:x:993:, and the host's render group is 104. The host has one Intel iGPU at /dev/dri/renderD128.
First remove any old lxc.cgroup2.devices.allow, lxc.mount.entry and lxc.hook.pre-start lines for /dev/dri from /etc/pve/lxc/105.conf. Both routes target the same path, and there is no reason to keep two. Then, on the host:
pct set 105 -dev0 /dev/dri/renderD128,gid=993
pct reboot 105Worked through the source, Proxmox now makes a node owned by host 100000:100993, mode 0660, and allows 226:128 read and write. Inside the container, ls -ln /dev/dri should show crw-rw---- 1 0 993 226, 128 for renderD128. Then, inside the container:
usermod -aG render jellyfin
systemctl restart jellyfin
runuser -u jellyfin -- /usr/lib/jellyfin-ffmpeg/vainfo --display drm --device /dev/dri/renderD128Jellyfin's docs say a line naming the iHD driver means both QSV and VA-API are available.
Now suppose the host's number went in by mistake, gid=104. The node would show group 104 inside the container, and 104 is not render there. The jellyfin user would be refused, but vainfo run as root would still pass, because container root owns the node. That is why the test above runs as jellyfin.
What we measured#
We have no Proxmox host and no Intel GPU in the lab. So we tested the part that decides "permission denied": how a device node's owner looks from inside a user namespace. On Linux 5.4 we made device nodes the way each route leaves them, using the machine's own DRM node (226:0). Then we opened them as uid 1000 from user namespaces with hand-written maps. The container's render GID was 993 and the host's was 109.
| Route, as reproduced | Container ls -ln | uid 1000 in group 993 |
|---|---|---|
| Old lines, host node as the host left it (0:109) | 65534 65534 | Permission denied |
| Old lines plus the forum's pre-start chown to 100000:100993 | 0 993 | Opens |
| Old lines plus a map passing container 993 to host 109 | 65534 993 | Opens |
dev0 copy, gid=993, default map (host 100000:100993) | 0 993 | Opens |
dev0 copy, host GID by mistake (gid=109, host 100000:100109) | 0 109 | Permission denied |
dev0 copy, gid=993, custom map (host 100000:109) | 0 993 | Opens |
Three more results. A uid 1000 process without group 993 was refused in every case. Container root with no groups opened the wrong-GID node, because it owns it. And after the forum's chown, a host process in the host's render group was refused a node it could open before.
The cgroup rule, pct itself and real transcoding were not tested, and those come from Proxmox's source and Jellyfin's docs.
Which route to use#
| Your setup | Route | Ruling from |
|---|---|---|
| Proxmox VE 8.2 or later | Resources, Add, Device Passthrough, GID = render GID inside the container | Roadmap 8.2, Jellyfin docs |
| Proxmox VE 8.1 | pct set <id> -dev0 /dev/dri/renderD128,gid=<GID> | Roadmap 8.1, pct.conf(5) |
| 8.0 or older, privileged container | Jellyfin's cgroup2 and mount entry lines | Jellyfin docs |
| 8.0 or older, must stay unprivileged | The same lines plus an lxc.idmap for the render group, not the chown hook | LXC manual, lab rows above |
| Moving from the old lines | Delete them, then add dev0 | pve-container source |
Making the container privileged on 8.1 or later buys nothing here, and it gives up the id shift. Docker in LXC or a VM on Proxmox explains why that shift is the boundary that matters on a shared kernel.
Docker inside the container#
If Jellyfin runs in Docker inside the LXC, dev0 gets the node into the LXC, and Docker needs it passed one level further. Jellyfin's Docker example uses devices: - /dev/dri/renderD128:/dev/dri/renderD128 and group_add with the render GID of the Docker host. Here the Docker host is the LXC, so that is the same 993. The Docker half is in GPUs, transcoding and local AI.
Proving the GPU is doing the work#
Jellyfin's docs give the check. Install intel-gpu-tools on the Proxmox host, not in the container. Play a video in the web client and force a transcode by picking a lower resolution or bitrate. Then run intel_gpu_top on the host. The docs describe the Video row as the QSV decoder or encoder workload. Render/3D carries QSV VPP and GPU compute work. If Video stays at 0% while a transcode runs, the CPU is doing it.
Inside the container, intel_gpu_top fails: a user in the forum thread got Failed to initialize PMU! (Operation not permitted) there. That error says nothing about whether transcoding works.
When this answer stops applying#
This page covers Intel iGPUs and Arc cards through /dev/dri and Jellyfin's Intel page. NVIDIA and AMD cards are not covered. A second GPU is renderD129, passed as dev1 with the same GID. The host must already show a renderD* node, and Jellyfin's docs say to install drivers on the host if it does not. Inside the container, Jellyfin still needs QSV or VA-API switched on in its settings, and OpenCL tone-mapping needs intel-opencl-icd. Everything about dev0 here comes from Proxmox's source and manual, read on 10 November 2026, not from a Proxmox host. Check ls -ln /dev/dri inside your own container before you trust the numbers on this page.
What to do next#
Run pveversion on the host. On 8.1 or later, read the render GID inside the container, delete the old lines, add dev0 with that number and restart. Then run vainfo as jellyfin, not as root, and watch the Video row on the host. If the media folders are the next permission error, Why an unprivileged LXC gets permission denied on a bind mount has the same arithmetic for files. And if you are still choosing a server, Jellyfin vs Plex covers which one is worth the passthrough.
Questions#
What render GID do I use for Jellyfin in a Proxmox LXC?
The number of the render group inside the container. Run getent group render in the container and use the third field. Jellyfin's docs say the same: GIDs can be looked up in /etc/group inside the LXC, and they give 989 as an example. Do not use the host's number, which can differ, and do not add 100000. Proxmox applies the container's id map to the gid= value itself.
Do I still need lxc.cgroup2.devices.allow with Proxmox device passthrough?
No. For every dev entry, Proxmox's LXC.pm writes lxc.cgroup2.devices.allow = c 226:128 rw itself, using the major and minor numbers it reads from the host's node. It writes a read-only rule instead if you set deny-write=1. Delete any old lxc.cgroup2.devices.allow, lxc.mount.entry and lxc.hook.pre-start lines for /dev/dri when you switch, since both routes target the same path.
Why does vainfo work but Jellyfin still transcodes on the CPU?
Usually because vainfo ran as root. Proxmox gives the passed-through node to container root by default, and the owner can open it whatever the group is. In our test, container root with no groups opened a node carrying the wrong GID. The jellyfin user can only open it through the group. Run runuser -u jellyfin -- /usr/lib/jellyfin-ffmpeg/vainfo --display drm --device /dev/dri/renderD128 to test as the service does.
Does Jellyfin need a privileged LXC for Intel Quick Sync on Proxmox?
Not on Proxmox VE 8.1 or later. The privileged requirement in Jellyfin's docs sits under "Proxmox VE 7 or Older", for the cgroup2 and mount entry lines. Those lines bind the host's own node, and its owner has no number in an unprivileged container. The dev0 option makes a new node with an owner the container can see, so the container can stay unprivileged.
Why does intel_gpu_top not work inside the LXC?
It is not meant to run there. Jellyfin's docs say to install intel-gpu-tools on the host system and run intel_gpu_top there during a transcode. A user in the Proxmox forum thread got Failed to initialize PMU! (Operation not permitted) running it inside the container. That error says nothing about whether transcoding works. Watch the Video row on the host instead.
Can two LXC containers share the same Intel iGPU?
Yes. Proxmox does not hand the host's node to the container. Its prestart hook makes a separate node for each container, under /var/lib/lxc/<id>/passthrough, from the host node's device numbers. Give each container its own dev0 line with its own render GID. Jellyfin's docs add that Intel iGPUs have no limit on concurrent encoding sessions, unlike NVIDIA's NVENC.
Sources#
- Jellyfin docs, HWA tutorial on Intel GPU (LXC on Proxmox, Docker, Verify on Linux)
- Proxmox VE 9.2.13, pct.conf(5), dev[n] option
- Proxmox VE roadmap, release notes 8.1 (dev options, API and CLI) and 8.2 (GUI)
- pve-container source, src/PVE/LXC.pm (create_passthrough_device_node, cgroup2 rule, root@pam check)
- pve-container source, src/lxc-pve-prestart-hook (passthrough tmpfs)
- pve-container source, src/lxc-pve-autodev-hook (bind mount into /dev)
- LXC, lxc.container.conf(5), lxc.mount.entry and lxc.hook.pre-start
- Proxmox forum, [Guide] Jellyfin + remote network shares + HW transcoding with Intel's QSV + unprivileged LXC (March 2024)
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.