Gaming in Proxmox LXC
My journey to run Steam with video output in an unprivileged LXC container with Nvidia GPU passthrough.
Why Even Bother?
As the proud owner of an old CRT TV scavenged from my parents' garage, I wanted to use it for something more than booting Mario on the NES once a year. It occurred to me: why not reminisce about old times while watching classic cartoons and anime, and maybe even play some retro games? So I bought HDMI to SCART dongle and started to think.
The challenge was finding an efficient way to run gaming and media applications without dedicating an entire virtual machine to occasional use. This led me to explore running a full desktop environment within an LXC container with GPU passthrough.
Hardware Setup
My machine is rather powerful for this endeavor, featuring:
- AMD EPYC 7551P
- 128 GB ECC RAM
- Nvidia RTX A400
- Nvidia RTX 3090
The system runs Proxmox 8.4.5 with kernel 6.12-12-pve. Since I have two GPUs, the obvious solution would be to pass one through to a VM and easily achieve what I want. However, I won't be using this daily, so why waste processing power on an idle VM? You might suggest simply turning off the VM when it's not needed, but I'm too lazy for that – I want to have my cake and eat it too. So how do we accomplish this with an LXC container?
Setting Up the LXC Container
I've been daily driving Arch Linux for quite some time, so the obvious choice for me is the archlinux-base_20240911-1_amd64 template. You should go with whatever distribution you're familiar with, but keep in mind that group mappings and package names will differ.
Device Passthrough
Proxmox 8.4.5 introduces a new method for passing devices to LXC containers, which we'll use instead of the previously viable mountpoint approach.
Let's add devices to my LXC by editing /etc/pve/lxc/xxx.conf:
dev0: /dev/dri/card2,gid=985,mode=0666
dev1: /dev/dri/renderD129,gid=989,mode=0666
dev10: /dev/snd/hwC1D0,gid=996
dev11: /dev/snd/pcmC1D7p,gid=996
dev12: /dev/snd/pcmC1D8p,gid=996
dev13: /dev/snd/pcmC1D9p,gid=996
dev2: /dev/nvidia1,mode=0666
dev3: /dev/nvidiactl,mode=0666
dev4: /dev/nvidia-modeset,mode=0666
dev5: /dev/nvidia-uvm,mode=0666
dev6: /dev/nvidia-uvm-tools,mode=0666
dev7: /dev/fb1,gid=985,mode=0666
dev8: /dev/snd/controlC1,gid=996
dev9: /dev/snd/pcmC1D3p,gid=996
What do these mappings accomplish? We want devices to be usable within the container, which requires proper group mapping between the host and container.
To check groups in your container, run:
getent group render
# or
cat /etc/group
The important groups to map are:
render:x:989:
video:x:985:
audio:x:996:
In my case, I'm passing through my RTX 3090, which is my second GPU. That's why you see nvidia1, card2, and renderD129 instead of nvidia0, card0, and renderD128.
I Use Arch BTW
Once the LXC container is running, let's start by refreshing the pacman keys:
pacman-key --init
pacman-key --populate archlinux
pacman-key --refresh-keys
Now update packages and install git:
pacman -Syuu
pacman -S --needed git base-devel
Finally, create the user account:
groupadd drakkein
useradd -m -g drakkein -d /home/drakkein drakkein
passwd drakkein
usermod -aG sudo,render,video,audio,git drakkein
From now on, I'll use yay for package management. You can use it as well or stick with pacman.
Nvidia Driver Installation
My host system has kernel modules for driver version 575.64.03, so we'll use the same version in the container:
curl https://us.download.nvidia.com/XFree86/Linux-x86_64/575.64.03/NVIDIA-Linux-x86_64-575.64.03.run -o NVIDIA-Linux-x86_64-575.64.03.run
chmod +x ./NVIDIA-Linux-x86_64-575.64.03.run
sudo ./NVIDIA-Linux-x86_64-575.64.03.run --no-kernel-modules --silent
# The Nvidia installer will complain about missing Vulkan ICD loader
yay -S vulkan-icd-loader
Time for a test, and voilà! My GPU is detected by the driver:
[drakkein@tvbox-arch ~]$ nvidia-smi
Fri Aug 1 19:06:56 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 575.64.03 Driver Version: 575.64.03 CUDA Version: 12.9 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 3090 Off | 00000000:21:00.0 On | N/A |
| 0% 53C P8 19W / 420W | 8MiB / 24576MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+
But how do we display something from the LXC container? Let's start by displaying some noise on the framebuffer:
cat /dev/urandom > /dev/fb1
Desktop Environment Setup
Since our GPU is working and displaying noise on the screen, it's time to install Xorg and Plasma. We'll follow the Arch Wiki for this. The installation might complain about existing files for mesa and libglvnd – the culprit is the Nvidia driver.
Additional packages I want to install are x11vnc steam firefox, so it's a good time to enable the multilib repository.
Time to prepare X11 by editing /etc/X11/xorg.conf and adding our Nvidia card to the configuration:
Section "ServerLayout"
Identifier "Layout0"
Screen 0 "Screen0" 0 0
InputDevice "Keyboard0" "CoreKeyboard"
InputDevice "Mouse0" "CorePointer"
EndSection
Section "Files"
EndSection
Section "InputDevice"
# generated from default
Identifier "Mouse0"
Driver "mouse"
Option "Protocol" "auto"
Option "Device" "/dev/mouse"
Option "Emulate3Buttons" "no"
Option "ZAxisMapping" "4 5"
EndSection
Section "InputDevice"
# generated from default
Identifier "Keyboard0"
Driver "kbd"
EndSection
Section "Monitor"
Identifier "Monitor0"
VendorName "Unknown"
ModelName "Unknown"
Option "DPMS"
EndSection
Section "Device"
Identifier "NvidiaCard"
Driver "nvidia"
VendorName "NVIDIA Corporation"
BoardName "GeForce RTX 3090"
BusID "PCI:33:0:0"
EndSection
Section "Screen"
Identifier "Screen0"
Device "Device0"
Monitor "Monitor0"
DefaultDepth 24
Option "AllowEmptyInitialConfiguration" "true"
Option "Stereo" "0"
Option "nvidiaXineramaInfoOrder" "DP-5"
Option "metamodes" "1024x768_60 +0+0"
Option "SLI" "Off"
Option "MultiGPU" "Off"
Option "BaseMosaic" "off"
SubSection "Display"
Depth 24
EndSubSection
EndSection
How Do We Start It?
In an LXC container, we don't have virtual terminals (VT), so the typical graphical login with a greeter won't work. We need to start X manually and launch Plasma. However, this creates another problem: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR are missing, so none of our systemd user units will launch. We can set them manually:
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
mkdir -p "$XDG_RUNTIME_DIR"
eval $(dbus-launch --sh-syntax --exit-with-session)
Unfortunately, it's still not working:
Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined (consider using --machine=<user>@.host --user to connect to bus of other user)
The solution is to either provide the machine parameter or use machinectl shell [email protected], which makes our user units work properly. Here is the Convinient script that I use to launch plasma.

What If Sound Isn't Working?
The likely reason is that no sink has been created for your audio device. In my case, I'm using the third HDMI port of my GPU, which corresponds to the second audio device.
To create a sink manually, create the file /etc/pipewire/pipewire.conf.d/99-hdmi-alsa-sink.conf:
context.objects = [
{
factory = adapter
args = {
factory.name = api.alsa.pcm.sink
node.name = "alsa-hdmi-sink"
node.description = "Manual HDMI Sink (hw:1,3)"
media.class = "Audio/Sink"
audio.position = "FL,FR"
api.alsa.path = "hw:1,3"
api.alsa.format = "S16LE"
api.alsa.rate = 48000
api.alsa.channels = 2
priority.session = 1000
node.exclusive = false
}
}
]
How to Remove System Policy Prompts
First, add your user to the network group. Then create a polkit rule at /etc/polkit-1/rules.d/50-org.freedesktop.NetworkManager.rules:
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 && subject.isInGroup("network")) {
return polkit.Result.YES;
}
});
Gaming Setup
For gaming, I installed steam, gamescope, and rpcs3. However, we need input devices, so the first step is to pass them to the LXC container:
dev14: /dev/input/js0,gid=994
dev15: /dev/input/event13,gid=994
Remember to add your user to the input group.

If you're wondering why my device shows as 8bitDo Virtual Gamepad, it's because Steam has a problem with reattaching disconnected devices. I decided to create a virtual device with uinput and mirror all events from my real device while ignoring disconnects. This also solves the issue with my 8BitDo Ultimate controller under Linux, where it immediately disconnects if nothing grabs its events.
Everything appears to be working, so I can finally create a systemd unit that will start Plasma after LXC launch:
[Unit]
Description=Start plasma after LXC start
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/startplasma-lxc
[Install]
WantedBy=multi-user.target
Gamescope Configuration
I want a Steam Deck-like experience using a gamepad, so my goal is to launch a gamescope session. After installing gamescope, mangohud, and lib32-mangohud, the first step is to create a new script at /usr/bin/gamescope-session. Besides launching Steam, we also move the cursor out of the way:
#!/bin/bash
xdotool mousemove 1023 767
gamescope --mangoapp -f -e -W 1024 -H 768 -- steam -gamepadui -steamos3 -steampal -steamdeck
Create /usr/bin/steamos-session-select to allow returning to Plasma:
#!/bin/bash
steam -shutdown
Create /usr/bin/steamos-update and /usr/bin/jupiter-biosupdate to mock SteamOS and Deck BIOS updates:
#!/bin/bash
exit 0;
For convenience, I also created a desktop shortcut for gamescope-session:
[Desktop Entry]
Exec=gamescope-session
Icon=steam
Name=Steam Gamescope
StartupNotify=true
Terminal=false
Type=Application


For now, we can exit Steam and continue with the setup.
EmuDeck Integration
The next step is to integrate emulation into Steam. For this, I'll use EmuDeck.
For RPCS3, we need to set a higher memory lock limit, so I need to update my LXC configuration. We also want to pin CPU cores for this LXC container:
lxc.prlimit.memlock: unlimited
lxc.cgroup.cpuset.cpus=0-15

Kodi Setup
Kodi setup is straightforward – all we need to install is kodi and the kodi-peripheral-joystick plugin. I self-host Jellyfin, so in my case, an additional dependency is the Jellyfin plugin.
All we need to do now is add a Kodi shortcut to Steam.
Final Configuration
Now we need to automatically launch our gamescope-session. I opted to do this via Plasma Settings → Startup and Shutdown → Autostart → Login Scripts.
