Proxmox Unprivelliged LXC with shared Intel ARC GPU for Plex or Jellyfin transcoding

This is to create Plex and/or Jellyfin using docker within an Unprivilleged LXC container using Proxmox with access to the LXC’s host Intel ARC GPU.

I’m using Debian Trixie LXC for this example. This assumes you have:

Check the render device group on the host

ls -l /dev/dri/render*


Example output:

crw-rw---- 1 root render 226, 128 Nov 16 21:02 /dev/dri/renderD128
crw-rw---- 1 root render 226, 129 Nov 16 21:02 /dev/dri/renderD129


This is for the iGPU and the ARC GPU, the ARC GPU is renderD129. Both are part of the device GROUP 226 this will be needed later on.

Find the GID of the render group on the host – HOST_GID

getent group render


Example output:

render:x:993:


We also need the render group of the LXC – LXC_GID

getent group render


Example output:

render:x:105:



On the proxmox host, edit the config for the lxc

nano /etc/pve/lxc/<CTID>.conf


Add these lines in, remembering:
GROUP: 226
LXC_GID: 105
HOST_GID: 993
SUBUID_START: 100000 (constant)
BASE_RANGE: 65536 (constant)

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.cgroup2.devices.allow: c 226:129 rwm
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD129 none bind,optional,create=file
lxc.idmap: u 0 100000 105
lxc.idmap: g 0 100000 105
lxc.idmap: u 105 993 1
lxc.idmap: g 105 993 1
lxc.idmap: u 106 100106 65430
lxc.idmap: g 106 100106 65430


This is to share both the iGPU and ARC GPU with 226 being the group.
The idmaps are rather complicated but you should be fine if you follow the rules

lxc.idmap: u 0 SUBUID_START BASE_RANGE = lxc.idmap: u 0 100000 105
lxc.idmap: g 0 SUBUID_START BASE_RANGE = lxc.idmap: g 0 100000 105

lxc.idmap: u LXC_GID HOST_GID 1 = lxc.idmap: u 105 993 1
lxc.idmap: g LXC_GID HOST_GID 1 = lxc.idmap: g 105 993 1

lxc.idmap: u (LXC_GID+1) (SUBUID_START + LXC_GID + 1) (BASE_RANGE - (LXC_GID+1)) = lxc.idmap: u 106 100106 65430
lxc.idmap: g (LXC_GID+1) (SUBUID_START + LXC_GID + 1) (BASE_RANGE - (LXC_GID+1)) = lxc.idmap: g 106 100106 65430


Save and exit the config.

We need to add the HOST_GID to the proxmox host subuid and subgid files

nano /etc/subuid


This should be as follows, then save and close

root:100000:65536
root:993:1


The same for the subgid

nano /etc/subgid
root:100000:65536
root:993:1


Restart the LXC container so the new config is loaded. Nearly there..

Docker Compose

For both Plex and Jellyfin the ARC GPU needs to be passed through into the container, add these lines into the docker compose file

    devices:
      - /dev/dri:/dev/dri

Plex

For Plex, go to Settings, Transcoder then scroll down to Hardware transcoding device your ARC should be there. Mine shows Intel DG2 [Arc A310]. If it’s not here, double check the idmaps in the LXC config from the proxmox host

Test the transcoding by playing any video, then in the Play Settings change the quality to something lower, then go to Activity, Dashboard and see if its transcoding with a hardware (hw) tag

NOTE: Hardware transcoding does require a subscription.
 

Jellyfin

For Jellyfin, top left 3 bars, Adminstration, Dashboard
Then Playback, Transcoding
Under Hardware Acceleration select Intel Quicksync (QSV)
With QSC Device enter /dev/dri/renderD129 for your ARC GPU
(or /dev/dri/renderD128 if you dont have iGPU)

To test play a video, go to the Play Settings, lower the quality, then click Playback Info
Look through the info for Play method Transcoding.
When Jellyfin transcoding is incorrectly setup videos wont play at all, check the idmaps in the LXC config from the proxmox host

Bypassing CGNAT for Plex with Tailscale funnels

Disclaimer: I’m currently on a static IP and run Plex using a lifetime Plex Pass, with remote access enabled. That said, I still understand the frustrations that come with CGNAT setups.

With the lack of IP address for IPv4 and the introduction of ISP CGNAT, this has created chaos with self hosting and using Plex.
Clouflare tunnels is good, but it’s also against their ToS for media streaming and download servers which you could find yourself without access. It’s currently not explictly against Tailscale ToS yet, but it’s not the recommended use for the funnel service.

Tailscale funnels to save the day! (..for now)

Essentially this opens up a service to the tailscale domain (.ts.net) which are public facing, this will ‘funnel’ all traffic into your deisgnated service when accessed.

First you’ll need to create an account with tailscale if you havent already.

Head over to the Admin Console and DNS


Hit the ‘Rename tailnet…” button until you get something cool 😉
My example shows cool-kid.ts.net
Scroll down and make sure MagicDNS and HTTPS Certificates are active


Install Tailscale on your VM or LXC, instructions can be found here.
Alternatively, use this script

curl -fsSL https://tailscale.com/install.sh | sh


If installing on an LXC you will need to add a few extra commands to the config in proxmox.

nano /etc/pve/lxc/<CTID>.conf
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file


Restart the LXC when the changes have been made.

For both VM and LXC now activate tailscale with:

tailscale up


Follow the link to authenticate tailscale, this should now show up in your admin console.

While still in VM / LXC shell we use the magic command to allow the funnel!

tailscale funnel --bg http://127.0.0.1:32400


Follow the instructions to accept the Funnel and keep both boxes ticked

Pay attention to the output of the command, it should look something like this

Available on the internet:

https://plex.cool-kid.ts.net/
|-- proxy http://127.0.0.1:32400

Funnel started and running in the background.
To disable the proxy, run: tailscale funnel --https=443 off


Make note of the webaddress as we will need to enter this into plex settings.

Navigate to your plex instance settings http://<YourIP>:32400/web and scroll down to Remote Access and click Disable Remote Access as this wont be needed anymore.

Navigate to Network and scroll down to the bottom, enter in the webaddress tailscale gave you in the output from the funnel command, you must include the :443 port at end for this to work.

https://plex.cool-kid.ts.net:443


Then hit save changes, and restart Plex for good measure.


Now your plex server will be accessible behind CGNAT with Tailscale Funnels.

The only downside – when looking at your dashboard, remote connections will not show on the graph.

If you also want to continue using tailscale for secure access to all your selfhosted apps, they offer a feature called Split DNS by domain. Which when connected to tailscale allows you to route all dns requests for your personal domain like richay.au into your PiHole / Adguard instance to redirect you to your internal reverse proxy. This stops unwanted access publicly to self-hosted services, but allows devices signed into tailscale to have access. My guide can be found here.

For public facing services like this blog, cloudflare tunnels is a great way to get around CGNAT.

Enjoy 🙂

Ansible – updating proxmox host kernel with LXC shared GPU

This is to automate the updating of proxmox host when there is a kernel update which will break the LXC link to the GPU.

It just requires you to reinstall the graphics driver and do a reboot otherwise.

This is after you have done an update / upgrade of your proxmox host.
You will have to change the IP Addresses for your setup.

For me..
10.77.69.2 – Proxmox Host
10.77.69.103 – LXC Plex

########
- hosts: nvidia
  become: true
  become_user: root
  tasks:
    - name: Wait for 10.77.69.2 to become available
      wait_for_connection:
        delay: 5
        timeout: 300

    - name: Check if NVIDIA kernel module is loaded
      shell: lsmod | grep -q '^nvidia'
      register: nvidia_module_check
      ignore_errors: true

    - name: Set NVIDIA module check result as fact
      set_fact:
        nvidia_module_rc: "{{ nvidia_module_check.rc }}"

    - name: Reinstall NVIDIA driver if module is not loaded
      shell: sh /root/NVIDIA-Linux-x86_64-535.154.05.run --silent
      args:
        executable: /bin/bash
      when: nvidia_module_check.rc != 0

    - name: Set fact if NVIDIA driver was installed
      set_fact:
        driver_installed: true
      when: nvidia_module_check.rc != 0

    - name: Reboot system if NVIDIA driver was reinstalled
      reboot:
      when: nvidia_module_check.rc != 0

    - name: Wait for 10.77.69.2 to become available after reboot
      wait_for_connection:
        delay: 10
        timeout: 600
      when: nvidia_module_check.rc != 0

########
- hosts: plex
  become: true
  become_user: root
  tasks:
    - name: Install NVIDIA driver in LXC
      shell: sh /root/NVIDIA-Linux-x86_64-535.154.05.run --no-kernel-module --silent
      args:
        executable: /bin/bash
      when: hostvars['10.77.69.2'].driver_installed | default(false)

    - name: Reboot 10.77.69.103
      reboot:
      when: hostvars['10.77.69.2'].driver_installed | default(false)

    - name: Wait for 10.77.69.103 to become available
      wait_for_connection:
        delay: 10
        timeout: 300
      when: hostvars['10.77.69.2'].driver_installed | default(false)


This will check to see if the kernels for nvidia (my gpu) has been loaded, if not it will reinstall in silent mode. This will also flag a GPU install in ansible to also reinstall the GPU driver in the LXC, only if it needs though.

Docker stack inc. ExpressVPN

docker-compose.yml

# Docker compose to set up containers for all services you need:
# VPN
#   Sonarr, Radarr, Lidarr, Qbittorrent
# Non-VPN
#   Plex, get_iplayer
# Before running docker-compose, you should pre-create all of the following folders.
# Folders for Docker State:
#  /volume1/dockerdata.             - root where this docker-compose.yml should live
#  /volume1/dockerdata/plex         - Plex config and DB
#  /volume1/dockerdata/sonarr       - Sonarr config and DB
#  /volume1/dockerdata/radarr       - Radarr config and DB
#  /volume1/dockerdata/lidarr       - Lidarr config and DB
#  /volume1/dockerdata/qbt          - QBitorrent config and DB
#  /volume1/dockerdata/prowlarr     - Prowlarr config and DB
#  /volume1/dockerdata/get_iplayer  - Get_iPlayer config and DB
# Folders for Media etc:
#  /volume1/photo                   - Root storage for photo files
#  /volume1/music                   - Root storage for music files
#  /volume1/video                   - Root storage for video files
#  /volume1/video/TV                - folder for Sonarr/Plex TV shows
#  /volume1/video/Films             - folder for Radarr/Plex Movies
#  /volume1/video/Downloads         - QBT folder for TV downloads (that Sonarr will process)
#  /volume1/video/FilmDownloads     - QBT folder for Movie downloads (that Radarr will process)
#  /volume1/video/MusicDownloads    - QBT folder for Music downloads (that Lidarr will process)
#  /volume1/video/Downloads/iPlayer - Destination folder for get_iplayer. See https://github.com/Webreaper/SonarrAutoImport
# 
# Note that Sonarr and Radarr don't necessarily need the VPN if you're using Prowlarr for the indexers. 
# But Prowlarr should be behind the VPN to avoid ISPs that block indexers.

services:
    expressvpn:
        image: polkaned/expressvpn
        container_name: expressvpn
        environment:
            - ACTIVATION_CODE=YOUR_CODE_HERE
        cap_add:
            - NET_ADMIN
        devices:
            - "/dev/net/tun:/dev/net/tun"
        privileged: true
        tty: true
        ports:
            # QBT UI Port
            - 8090:8090
            # Bittorrent port
            - 6881:6881
            - 6881:6881/udp
            # Web port
            - 8080:8080
            - 9117:9117
            # Sonarr port
            - 8989:8989
            # Radarr port
            - 7878:7878
            # Lidarr port
            - 8686:8686
            # Prowlarr port
            - 9696:9696
        command: /bin/bash
        restart: unless-stopped

    qbittorrent:
        image: linuxserver/qbittorrent
        container_name: qbittorrent
        environment:
            - PUID=1024
            - PGID=101
            - TZ=Europe/London
            - UMASK_SET=022
            - WEBUI_PORT=8090
        volumes:
            - /volume1/dockerdata/qbt/config:/config
            - /volume1/video/Seeding:/downloads
            - /volume1/video/Downloads:/tvdownloads 
            - /volume1/video/FilmDownloads:/filmdownloads 
            - /volume1/video/MusicDownloads:/musicdownloads
        network_mode: service:expressvpn
        restart: unless-stopped

    prowlarr:
        image: linuxserver/prowlarr:develop
        container_name: prowlarr
        environment:
            - PUID=1024
            - PGID=101
            - TZ=GB
        volumes:
            - /volume1/dockerdata/prowlarr:/config
        restart: unless-stopped
        network_mode: service:expressvpn
        depends_on:
            - expressvpn
            - qbittorrent
            
    sonarr:
        image: linuxserver/sonarr:latest
        container_name: sonarr_v3
        environment:
            - PUID=1024
            - PGID=101
            - TZ=GB
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /volume1/dockerdata/sonarr/config:/config
            - /volume1/video/TV:/tv
            - /volume1/video/Downloads:/downloads
        restart: unless-stopped
        network_mode: service:expressvpn
        depends_on:
            - prowlarr
            - qbittorrent

    radarr:
        image: linuxserver/radarr:latest
        container_name: radarr_v3
        environment:
            - PUID=1024
            - PGID=101
            - TZ=GB
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /volume1/dockerdata/radarr3/config:/config
            - /volume1/video/Films:/movies
            - /volume1/video/FilmDownloads:/downloads
        restart: unless-stopped
        network_mode: service:expressvpn
        depends_on:
            - prowlarr
            - qbittorrent

    lidarr:
        image: linuxserver/lidarr
        container_name: lidarr
        environment:
            - PUID=1024
            - PGID=101
            - TZ=GB
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /volume1/dockerdata/lidarr/config:/config
            - /volume1/music:/music
            - /volume1/video/MusicDownloads:/downloads
        restart: unless-stopped
        network_mode: service:expressvpn
        depends_on:
            - prowlarr
            - qbittorrent
    plex:
        container_name: plex
        image: linuxserver/plex
        devices:
            - /dev/dri:/dev/dri
        privileged: true
        environment:
            PGID: '101'
            PUID: '1024'
            VERSION: docker
        network_mode: host
        restart: unless-stopped
        volumes:
            - /volume1/dockerdata/plex:/config:rw
            - /volume1/video/TV:/tv:rw
            - /volume1/video/Films:/movies:rw
            - /volume1/music:/music:rw
    get_player:
        container_name: get_iplayer
        image: kolonuk/get_iplayer
        environment:
            PGID: '101'
            PUID: '1024'
        ports:
            - 8181:8181/tcp
        restart: unless-stopped
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /volume1/dockerdata/get_iplayer:/root/.get_iplayer:rw
            - /volume1/video/Downloads/iPlayer:/root/output:rw  

Docker Compose for Entertainment Stack

My very own docker compose stack for entertainment.

They are all in the same network of ‘entertainment’ so linking apps together is as easy as http://prowlarr:9696 – no need for ip address of prowlarr 🙂

# bazarr
# jellyfin
# overseerr
# plex
# prowlarr
# radarr
# sonarr
# tautulli
# watchtower

version: "3.6"

services:          
    bazarr:
        image: lscr.io/linuxserver/bazarr:latest
        container_name: bazarr
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
        volumes:
            - bazarr_config:/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        ports:
            - 6767:6767
        restart: unless-stopped
        networks:
            - myNetwork
    
    jellyfin:
        image: lscr.io/linuxserver/jellyfin:latest
        container_name: jellyfin
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
            - JELLYFIN_PublishedServerUrl=192.168.0.5 # optional
        volumes:
            - jellyfin_config:/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        ports:
            - 8096:8096
            - 8920:8920 # optional
            - 7359:7359/udp # optional
            - 1900:1900/udp # optional
        restart: unless-stopped
        networks:
            - myNetwork
    
    overseerr:
        container_name: overseerr
        hostname: overseerr
        image: sctx/overseerr:latest
        restart: unless-stopped
        ports:
            - 5055:5055
        volumes:
            - overseerr_config:/app/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        environment:
            - LOG_LEVEL=debug
            - TZ=Australia/Perth
        networks:
            - myNetwork
                     
    plex:
        image: lscr.io/linuxserver/plex:latest
        container_name: plex
        network_mode: host
        ports:
            - 32400:32400
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
            - VERSION=docker
            - PLEX_CLAIM= # optional
        volumes:
            - /mnt/plex:/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        restart: unless-stopped
        # networks:
        #    - myNetwork
                    
    prowlarr:
        image: lscr.io/linuxserver/prowlarr:latest
        container_name: prowlarr
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
        volumes:
            - prowlarr_config:/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        ports:
            - 9696:9696
        restart: unless-stopped
        networks:
            - myNetwork
            
    radarr:
        image: lscr.io/linuxserver/radarr:latest
        container_name: radarr
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
        volumes:
            - radarr_config:/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        ports:
            - 7878:7878
        restart: unless-stopped
        networks:
            - myNetwork
            
    sonarr:
        image: lscr.io/linuxserver/sonarr:latest
        container_name: sonarr
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
        volumes:
            - sonarr_config:/config
            - /mnt/media:/mnt/media
            - /mnt/storage:/mnt/storage
            - /mnt/downloads:/mnt/downloads
        ports:
            - 8989:8989
        restart: unless-stopped
        networks:
            - myNetwork
            
    tautulli:
        image: lscr.io/linuxserver/tautulli:latest
        container_name: tautulli
        environment:
            - PUID=1000
            - PGID=1000
            - TZ=Australia/Perth
        volumes:
            - tautulli_config:/config
        ports:
            - 8181:8181
        restart: unless-stopped
        networks:
            - myNetwork
        
    watchtower:
        container_name: watchtower
        hostname: watchtower
        image: containrrr/watchtower
        environment:
            - TZ=Australia/Perth
        restart: unless-stopped
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
        command: --cleanup
            
networks:
    myNetwork:
        name: entertainment     
           
volumes:
    bazarr_config:
    jellyfin_config:
    overseerr_config:
    prowlarr_config:
    radarr_config:
    sonarr_config:
    tautulli_config: