Background

Given some recent bad news about Plex and those privacy-invading “Your Week in Review” emails, I decided to take another stab at setting up a Jellyfin server on my home network. At some point in the future I may decide to finally give up on using Plex day-to-day, and it would be great to have a ready alternative set up and running. Time to crack open the Ansible playbooks and spin up a container1!

For this build I’m using an Ubuntu 22.04 LXC template – the standard one you can find via the Proxmox web GUI. I create most of my homelab infra using Terraform2; see main.tf in this gist.3 Note in particular I have set unprivileged = true, making this an unprivileged container.

Using an unprivileged container has one major advantage, which is security. According to Proxmox docs:

These kind of containers use a new kernel feature called user namespaces. All of the UIDs (user id) and GIDs (group id) are mapped to a different number range than on the host machine, usually root (uid 0) became uid 100000, 1 will be 100001 and so on. This means that most security issues (container escape, resource abuse, …) in those containers will affect a random unprivileged user, even if the container itself would do it as root user, and so would be a generic kernel security bug rather than an LXC issue. The LXC team thinks unprivileged containers are safe by design.

The tradeoff for this advantage is complexity. We need to jump through a few hoops to ensure Jellyfin inside our unprivileged container can access the resources it needs on the host machine. There is a convenient way to ensure the unprivileged container can talk only to some specific resources on the host, versus a privileged container which has many fewer restrictions.

Create the container

Allow the container to boot and ensure you can SSH in. As mentioned in the previous section the container is defined in the main.tf file shared in the Github Gist for this project.

Then, let’s run an Ansible playbook against it to install some common bits of software I prefer to have in place (vim, htop, etc) and also install and configure Jellyfin. I have put pieces of that playbook that configure Jellyfin in the same gist. Since we’re not focused on Ansible in this post, I’ll skip the details of that playbook and how it all works. You don’t need to use an Ansible playbook to install and configure Jellyfin; I just have a preference for doing it that way these days.

Modify the container

With Jellyfin installed by the playbook, go ahead and shut down the container. It cannot be running when we modify it’s configuration file.

My config was located at /etc/pve/lxc/111.conf. Yours will most likely differ, unless your machine ID 111 matches my own!

Open it in your favorite editor (I prefer vim). We’re going to add a few lines to the container config file on the Proxmox host which will pass through the Intel QuickSync device from the Proxmox host machine into the unprivileged LXC.

I added the following lines. I’ll explain what each does below.

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=file
lxc.idmap: u 0 100000 65536
lxc.idmap: g 0 100000 118
lxc.idmap: g 118 103 1
lxc.idmap: g 119 100119 65417

Here’s what each line is doing4.

  1. Will give our LXC access to the card0 device. The 226:0 is identifying the device by its major/minor numbers. Important: you can validate the correct values for your hardware by running ls -lna /dev/dri on your host machine that has the Intel CPU (e.g. the Proxmox host machine).
  2. Will give our LXC access to the renderD128 device. The 226:128 is identifying the device by its major/minor numbers.
  3. Will bind mount the renderD128 device from the Proxmox host to the LXC.
  4. Map user IDs 0-65536 inside the LXC to user IDs 100000-165535 outside the container.
  5. Map group IDs 0-118 inside the LXC to group IDs 10000-100118 outside the container.
  6. Map group ID 118 inside the LXC to group ID 103 outside the container.
  7. Map group IDs 119-65536 (= 65417 + 119) inside the LXC to group IDs 100119-165535 outside the container.

Lines 1-3 are mapping our Intel QuickSync device from outside the container to inside the container, so that Jellyfin inside the container can access it. But simply passing the device into the container is not enough. We must ensure that the user/group inside the container has permissions to use that device.

Lines 4-7 are where this magic happens, with the lxc.idmap statements.

Line 4 is doing nothing for us here. It is just mapping all user IDs as they would normally map in a plain vanilla LXC setup. I believe we could omit this line and everything else would work fine, but when I am mapping group IDs I like to also list out the user ID mapping, for completeness sake.

Lines 5-7 map all group IDs per normal, except group 118 inside the container gets mapped to group 103 outside the container.

How the heck did I know to do this for 118 in the container and 103 outside of the container??

  • Inside the container and once Jellyfin is installed, run id jellyfin. For my setup I get id=110(jellyfin) gid=118(jellyfin) groups=118(jellyfin),44(video),108(render). Therefore, Jellyfin is in group 118.
  • Outside the container on the Proxmox host, run ls -lna /dev/dri and note the group which owns the renderD128 device. On my Proxmox machine it is 103.

Therefore, I want group 118 inside the container to map to group 103 outside the container. This ensures that any user in group 118 inside the container (which is the Jellyfin group) has permission to act as group 103 outside the container (which is the group that owns renderD128, our Intel QuickSync device).

Save your updated container config file.

Cool! Now our unprivileged container can access that device…and really, only that device on the host.

Tell Proxmox we’re doing this

One last step, we must modify /etc/subgid on the Proxmox host. We need to tell Proxmox that we’re going to do this custom user and group mapping. If you miss doing this, you will get an error when attempting to start the container.

Add to /etc/subgid:

root:103:1

The first column is the host user (root which creates the LXC), the second is the start of the host id range to be mapped (we’re mapping group 103 on the host), and the third is the number of IDs that can be allocated (we just need the one group). You can reference the Proxmox docs on unprivileged LXCs, as well.

Start it up

Go ahead and start up the LXC again. Check for errors. If you made a syntax error when editing the LXC config or did not update /etc/subgid, you will see an error in the Proxmox GUI.

If everything worked, you can now SSH back into the Jellyfin LXC and check to ensure that you can see the device by running ls -lna /dev/dri/renderD128. I get:

crw-rw---- 1 65534 118 226, 128 Nov 15 19:03 /dev/dri/renderD128

There is no valid user who owns the device (which is fine), but group 118 (jellyfin) inside the container now owns this device we mapped in from the host. Great! Now Jellyfin can access the Intel QuickSync engine.

The end

I’ll leave the Hardware Acceleration setup via Jellyfin’s web GUI to their official documentation.

Before you go, one quick note about how we verify hardware acceleration is working properly. The Jellyfin docs will ask you to use the program intel_gpu_top, but in an unprivileged LXC you will not be able to run this command. Instead, you must run it on the Proxmox host itself. You can follow the Jellyfin instructions to install the intel-gpu-tools package and then run intel_gpu_top - simply run it on the Proxmox host.

Any questions or feedback, find me on Mastodon @matt@toot.mattedwards.org.


  1. The reason for this is I can easily share the Intel QuickSync portion of my CPU (an i5-11400) with one or more LXCs, and I already have Plex running in one of those LXCs. LXCs are a container technology. Learn more here↩︎

  2. With the release of Proxmox 8, I have heard that the Telmate/proxmox provider no longer works. There is a new provider bpg/proxmox which I need to test out as a replacement. ↩︎

  3. If you choose to adapt this for your own needs, double check any variables I have set to “XXX” and replace with values matching your own setup. ↩︎

  4. I cobbled together this setup from this excellent gist which does a similar passthrough but for Plex, and the official Jellyfin docs which explain how to enable Intel QuickSync passthrough on a Proxmox LXC. Note that the Jellyfin docs also state you must run Jellyfin in a privileged LXC. However, we can make it work in an unprivileged one! ↩︎