GPU passthrough to an LXC container sounds like a smaller version of passing a PCI device to a virtual machine. It is not. A VM can take exclusive control of a GPU through VFIO, while an LXC container shares the Proxmox host kernel and normally receives access to selected device nodes such as /dev/dri/renderD128 or /dev/nvidia0. The host still owns the real driver and the hardware.
That distinction is the key to a setup that survives reboots and remains understandable. The reliable path is to make the GPU work on the Proxmox host first, pass only the device nodes the workload needs, assign them to the correct group inside an unprivileged container, and test access as the service account. If those layers are handled in order, hardware transcoding, inference, and other GPU workloads do not require a privileged container or a collection of legacy LXC overrides.
- How LXC GPU access differs from VM PCI passthrough
- How to identify the minimum device nodes a workload needs
- How to verify the GPU and driver on the Proxmox host
- How to configure native device passthrough for an unprivileged LXC
- How Intel, AMD, and NVIDIA setups differ
- How to validate a real workload and keep the setup maintainable
Start with the correct passthrough model
An LXC container is an isolated Linux userspace running on the host kernel. It does not boot its own kernel and cannot load a different kernel driver for the GPU. When a process inside the container opens a passed-through device node, its requests ultimately reach the driver already loaded by the Proxmox host.
This creates a very different operating model from PCI passthrough to a KVM virtual machine:
| Question | LXC device access | VM PCI passthrough |
|---|---|---|
| Who owns the kernel driver? | The Proxmox host | The guest operating system |
| Is the whole PCI device assigned? | No, selected device nodes are exposed | Normally yes |
| Can the host still use the GPU? | Often yes | Normally no while assigned |
| Is VFIO normally required? | No | Yes |
| Can multiple workloads share it? | Often, subject to driver and workload limits | Not without mediated or virtual GPU support |
The current Proxmox container configuration supports native dev[n] entries with a device path, UID, GID, access mode, and optional write restriction. This is preferable to copying old lxc.cgroup2.devices.allow and lxc.mount.entry fragments because Proxmox creates the container-side device node with the ownership you specify. It also works cleanly with unprivileged containers. Proxmox VE 9 removed cgroup v1 entirely, making legacy cgroup recipes an especially poor foundation for a new setup.
If the GPU is bound to vfio-pci, the normal host graphics driver cannot expose the render or NVIDIA device nodes that an LXC needs. Choose either host-driven container access or exclusive VM passthrough for a given device.
Decide what the workload needs before exposing devices
Passing an entire device directory is easy, but it gives the container more access than many workloads require. Start with the application, then choose the smallest useful interface.
For Intel and AMD graphics using the Linux Direct Rendering Manager, the render node is usually the correct starting point. The Linux kernel documentation for render nodes describes them as a lower-privilege interface for rendering and compute clients: they do not provide display modesetting or the privileged operations available through a primary cardN node.
| Device | Typical purpose | Pass it when |
|---|---|---|
/dev/dri/renderD128 |
VA-API, Quick Sync, render, and some compute workloads | The application performs headless acceleration |
/dev/dri/card0 |
Primary DRM and display or modesetting operations | The application explicitly requires it |
/dev/kfd |
AMD compute through HSA/ROCm | The compute stack documents it as a requirement |
/dev/nvidia* |
NVIDIA management, CUDA, NVDEC, and NVENC interfaces | The required NVIDIA userspace stack is installed |
A media server normally needs a render node, not control over a physical display. A machine-learning workload may need additional vendor-specific devices. A desktop environment inside a container is a different project and can justify card0, but it should not be the default for a headless transcode service.
Device access is not the same as resource isolation. Two containers may be able to open the same render node, but they still compete for GPU engines, memory bandwidth, and vendor-specific limits. LXC does not automatically give each workload a guaranteed share of the hardware.
Prove that the GPU works on the Proxmox host
Do not begin with the container. If the host has not loaded the correct driver or created the expected device nodes, no LXC configuration can repair the lower layer.
If the node itself is not yet installed and updated, start with Installing Proxmox VE for the First Time before adding hardware access to a workload.
Run the following commands as root on the Proxmox host:
pveversion -v
lspci -nnk | grep -A3 -Ei 'vga|3d|display'
ls -l /dev/dri
The lspci output should show the expected GPU and a kernel driver such as i915, xe, amdgpu, or nvidia. It should not show vfio-pci for a GPU intended to serve an LXC. Intel and AMD systems should normally expose at least one render node:
stat -c '%n mode=%a owner=%U group=%G major=%t minor=%T' /dev/dri/renderD*
Do not assume that the first GPU is always renderD128. Systems with an integrated GPU, a discrete GPU, or devices that initialize in a different order can expose renderD129 or another number. Match the render node to the physical device before assigning it. The links under /dev/dri/by-path/ and the corresponding /sys/class/drm/ entries help establish that relationship.
For NVIDIA, verify the driver before looking at the container:
nvidia-smi
ls -l /dev/nvidia* /dev/dri/renderD* 2>/dev/null
If nvidia-smi fails on the host, stop there. Fix the host driver, kernel-module, firmware, or Secure Boot issue first. Installing more packages inside an LXC cannot compensate for a broken host driver.
Also review recent driver messages after boot or a kernel update:
dmesg --level=err,warn | grep -Ei 'i915|xe|amdgpu|nvidia|drm'
Warnings are not automatically failures, but firmware load errors, initialization failures, and driver/library mismatches deserve attention before the device becomes part of a service dependency.
Prepare an unprivileged container and its groups
Proxmox creates new containers as unprivileged by default. In an unprivileged LXC, root inside the container maps to an unprivileged ID on the host, reducing the impact of many container escape and filesystem permission problems. The official Proxmox container guide describes privileged containers as appropriate only for trusted environments, so switching to one merely to make a GPU permission error disappear is the wrong first fix.
This guide uses 900 as a documentation-only container ID. Replace it with the ID of your test container:
CTID=900
pct config "$CTID" | grep -E '^(hostname|ostype|unprivileged):'
Confirm that the configuration contains:
unprivileged: 1
Install the application or at least create the groups it will use before assigning the device. On a Debian-based container, inspect the common graphics groups from the Proxmox host:
pct exec "$CTID" -- getent group render
pct exec "$CTID" -- getent group video
If the render group does not exist, create it inside the container and query it again:
pct exec "$CTID" -- groupadd --system render
pct exec "$CTID" -- getent group render
If the workload needs card0 or NVIDIA devices and the video group is also missing, create it in the same way:
pct exec "$CTID" -- groupadd --system video
Capture the numeric IDs from inside the container, not from the Proxmox host:
RENDER_GID=$(pct exec "$CTID" -- getent group render | cut -d: -f3)
VIDEO_GID=$(pct exec "$CTID" -- getent group video | cut -d: -f3)
printf 'render=%s video=%s\n' "$RENDER_GID" "$VIDEO_GID"
The host and container can assign different numeric IDs to groups with the same name. Native Proxmox device passthrough solves this cleanly by creating the device node inside the LXC with the GID requested in the container configuration.
Pass an Intel or AMD render node with the native Proxmox interface
The web interface is the clearest route for a first setup:
- Stop the container.
- Open the container and select
Resources. - Choose
Add, thenDevice Passthrough. - Select
/dev/dri/renderD128, or the render node identified earlier. - Open the advanced options and set the GID to the numeric
renderGID from inside the container. - Use mode
0660so only the owner and assigned group receive read/write access. - Start the container and verify the device before configuring the application.
The equivalent CLI workflow is explicit and easy to audit. Check first that dev0 is not already used, then stop the container and add the render node:
pct config "$CTID" | grep '^dev' || true
pct stop "$CTID"
pct set "$CTID" --dev0 "path=/dev/dri/renderD128,uid=0,gid=${RENDER_GID},mode=0660"
pct start "$CTID"
If the workload genuinely requires the primary DRM node, pass it separately using the container's video GID:
pct stop "$CTID"
pct set "$CTID" --dev1 "path=/dev/dri/card0,uid=0,gid=${VIDEO_GID},mode=0660"
pct start "$CTID"
Use the next free devN index if dev0 or dev1 already belongs to another device. Do not overwrite an existing mapping without understanding what it serves.
Confirm both the Proxmox configuration and the container-side result:
pct config "$CTID" | grep '^dev'
pct exec "$CTID" -- ls -l /dev/dri
pct exec "$CTID" -- stat -c '%n mode=%a owner=%U group=%G' /dev/dri/renderD128
Next, add the service account to the group that owns the render node. This example uses Jellyfin, but the same rule applies to Frigate, HandBrake workers, inference services, or another daemon:
SERVICE_USER=jellyfin
pct exec "$CTID" -- usermod -aG render "$SERVICE_USER"
pct exec "$CTID" -- id "$SERVICE_USER"
When card0 is also mapped, add the service to both groups with usermod -aG render,video "$SERVICE_USER" instead.
Restart the service, or restart the container if that is simpler during initial validation. Existing processes do not automatically receive supplementary groups added after they started.
Finally, test the device as the service account rather than as root:
pct exec "$CTID" -- runuser -u "$SERVICE_USER" -- sh -c 'test -r /dev/dri/renderD128 && test -w /dev/dri/renderD128'
No output and an exit status of zero means the service account has both read and write access. If root can open the device but the service cannot, the passthrough itself is probably working and the remaining problem is group ownership or process membership.
World-writable device nodes hide group mistakes by granting every process in the container GPU access. Mode 0660 plus a dedicated group keeps the permission boundary visible and survives troubleshooting better than a broad workaround.
Install the user-space stack inside the container
Passing a device node only opens a path to the host driver. The application still needs compatible user-space libraries inside the LXC. These packages expose APIs such as VA-API, Quick Sync, Vulkan, CUDA, NVDEC, or NVENC to the workload.
For a current Debian container using a modern Intel GPU, a basic VA-API validation stack is:
pct exec "$CTID" -- apt update
pct exec "$CTID" -- apt install -y vainfo intel-media-va-driver
For AMD video acceleration through Mesa:
pct exec "$CTID" -- apt update
pct exec "$CTID" -- apt install -y vainfo mesa-va-drivers
Debian provides the intel-media-va-driver and mesa-va-drivers packages used in these examples. Add mesa-vulkan-drivers only if the workload actually needs Vulkan. Application-specific distributions may bundle their own FFmpeg and media libraries; Jellyfin's Intel acceleration guide, for example, documents its own package, Proxmox LXC, and validation paths. Prefer the application's supported stack over mixing unrelated FFmpeg builds and driver packages.
Run vainfo against the render node as the real service account:
pct exec "$CTID" -- runuser -u "$SERVICE_USER" -- vainfo --display drm --device /dev/dri/renderD128
A useful result lists a VA-API driver and supported codec profiles. A simple ffmpeg -hwaccels is weaker evidence because it reports what the binary was compiled to support, not whether the current process can open this GPU.
Treat NVIDIA as a separate driver path
NVIDIA does not normally use only one DRM render node. Depending on the workload and driver, it may require /dev/nvidia0, /dev/nvidiactl, /dev/nvidia-uvm, /dev/nvidia-uvm-tools, or additional nodes. Inspect what exists on the host and follow the requirements of the CUDA, NVENC, or application stack you are deploying.
The native Proxmox method still applies. The following example passes a minimal set commonly needed by NVIDIA compute or media workloads, but the exact list must match the host driver and application. Inspect pct config "$CTID" first and replace dev0, dev1, and dev2 with free indexes when necessary:
NVIDIA_GID=$(pct exec "$CTID" -- getent group video | cut -d: -f3)
pct stop "$CTID"
pct set "$CTID" --dev0 "path=/dev/nvidia0,uid=0,gid=${NVIDIA_GID},mode=0660"
pct set "$CTID" --dev1 "path=/dev/nvidiactl,uid=0,gid=${NVIDIA_GID},mode=0660"
pct set "$CTID" --dev2 "path=/dev/nvidia-uvm,uid=0,gid=${NVIDIA_GID},mode=0660"
pct start "$CTID"
Pass an additional NVIDIA node only when it exists and the workload requires it. Native path-based entries avoid hard-coding character-device major numbers, which can vary for modules such as UVM.
The NVIDIA kernel module belongs on the Proxmox host. The container needs compatible user-space libraries and utilities, but it should not attempt to replace or build the host kernel module from inside LXC. NVIDIA's container architecture separates the host driver from libraries and device injection for this reason, and its current toolkit can support LXC without the Docker-specific runtime layer.
Verify the result inside the container:
pct exec "$CTID" -- nvidia-smi
If it reports a driver/library version mismatch, compare the host kernel driver with the user-space NVIDIA libraries visible inside the LXC. Reinstalling random packages or exposing more device nodes is not a substitute for aligning those versions.
Validate the workload, not just the device file
A device listed by ls is only the first checkpoint. A complete validation moves through four layers:
- The Proxmox host loads the correct kernel driver and can use the GPU.
- The expected device node appears inside the LXC with restrictive ownership.
- The service account can open it and the user-space API initializes.
- A real workload uses the intended hardware engine.
For a media server, trigger a transcode rather than playing a file that the client can direct-stream. Review the application's FFmpeg or transcode log and confirm that it selected VA-API, QSV, NVENC, or the expected vendor path. On Intel systems, intel_gpu_top on the Proxmox host can show activity in the video engines. On NVIDIA systems, nvidia-smi or nvidia-smi dmon can show the process and engine load.
Do not rely on lower CPU usage alone. Direct playback, caching, codec choice, and subtitle handling can all change CPU use without proving that the GPU performed the decode and encode stages. The application log plus vendor telemetry is stronger evidence.
Test again after a full container restart and a full Proxmox host reboot. A setup that works only until the next boot usually depends on a temporary permission change, a manually created device node, or a driver module that is not loading consistently.
Plan for updates, backups, and migration
GPU access makes an LXC dependent on hardware outside its root filesystem. The container backup can preserve its Proxmox configuration, packages, and application data, but it does not contain the host GPU driver, firmware, or the physical device.
Treat the container as tied to a compatible node unless another Proxmox host exposes the same required device paths and a compatible driver stack. Before migration or restore, verify the destination GPU, device names, groups, and userspace compatibility. A restored container may start successfully while its accelerated workload silently falls back to the CPU.
After Proxmox kernel, firmware, Mesa, or NVIDIA updates, repeat the shortest useful checks:
lspci -nnk | grep -A3 -Ei 'vga|3d|display'
ls -l /dev/dri /dev/nvidia* 2>/dev/null
pct exec "$CTID" -- sh -c 'ls -l /dev/dri /dev/nvidia* 2>/dev/null'
Then run the vendor API test and one real application workload. This takes a few minutes and catches driver changes before the next important transcode or compute job.
Troubleshoot the layer that actually failed
Most failed GPU passthrough attempts become easier once the symptom is assigned to the correct layer:
| Symptom | Likely layer | First check |
|---|---|---|
No /dev/dri/renderD* on the host |
Firmware or host driver | BIOS GPU state, lspci -nnk, and driver logs |
The LXC does not start after adding devN |
Invalid or missing host path | The exact device path on the Proxmox host |
| The device works as root but not as the service | Container permissions | Device GID, service groups, and service restart |
vainfo cannot initialize the device |
User-space driver or permissions | Run it as the service user and inspect the selected VA driver |
nvidia-smi reports a version mismatch |
NVIDIA kernel/userspace alignment | Host module version and container library version |
| The application still uses the CPU | Application or codec configuration | Actual transcode logs and supported codec profiles |
Avoid broad fixes such as a privileged container, an unconfined AppArmor profile, or world-writable devices until the failing layer is understood. Those changes can make a symptom disappear while weakening isolation and leaving the real dependency undocumented.
Conclusion
GPU passthrough to a Proxmox LXC is not a miniature VFIO project. It is controlled device access across a shared kernel boundary. The host loads and owns the driver, Proxmox creates selected device nodes inside the container, group permissions decide which process can open them, and user-space libraries translate application requests into the driver's API.
The cleanest setup is therefore the least dramatic one: an unprivileged container, one render node when that is sufficient, mode 0660, a deliberate service group, and a real workload test after every important driver change. That approach gives a media or compute service hardware acceleration without turning a permission problem into a permanent security exception.
FAQ
Do I need IOMMU enabled for GPU access in an LXC?
Not for normal device-node passthrough. The Proxmox host keeps control of the GPU driver, so the LXC does not use VFIO to claim the PCI device. IOMMU is important for PCI passthrough to a VM and may still be useful for other platform features, but it is not the mechanism described in this guide.
Can the Proxmox host and multiple LXC containers share one GPU?
Often yes, especially through a DRM render node, because the host driver schedules work from multiple processes. Actual concurrency, memory use, codec-session limits, and stability depend on the GPU, driver, and workload. Sharing access does not guarantee fair performance isolation.
Why does the GPU work as root but not for my application?
The service account probably does not belong to the group assigned to the device node, or the service has not been restarted since its group membership changed. Compare the device GID with id <service-user> inside the container and test the node as that user.
Does GPU passthrough require a privileged LXC container?
No on current Proxmox VE releases using native Device Passthrough. An unprivileged container can receive a device node with an explicit UID, GID, and mode. Vendor-specific edge cases may need additional work, but privileged mode should not be the default permission fix.
Should I use an LXC or a VM for a GPU workload?
Use LXC when low overhead, Linux-only workloads, and host-managed shared acceleration fit the requirement. Use a VM when the guest needs its own driver, exclusive control of the PCI device, stronger kernel isolation, Windows support, or a configuration that depends on full PCI behavior.



