𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme
Connect With Me on LinkedIn Buy Me a Coffee

Homelab

Plex Intel GPU Transcoding

Deploying Plex Media Server on Kubernetes with Intel Arc GPU Hardware Transcoding

Overview

Deploying Plex Media Server with Intel Arc GPU hardware transcoding. This is one of the primary goals of the entire homelab.

Tip:Having trouble? See v0.10.0 for what your setup should look like after completing this article.

Before You Begin

Prerequisites

Why This Approach

GPU access: The official Plex Helm chart doesn't support DRA (Dynamic Resource Allocation). Instead, we use hostPath volume mounts1 to expose /dev/dri directly to the container.

Storage strategy:

  • Config data: Longhorn distributed storage (replicated, backed up)
  • Media files: Longhorn for initial testing, NFS from NAS for bulk media later

If migrating from a previous setup, you may need to restore media from backups.

What's Not In Scope

DRA GPU Support: The current hostPath approach works but has limitations. Dynamic Resource Allocation (DRA)2 provides a better solution:

CapabilityhostPath (current)DRA (future)
Resource accountingNoYes
Scheduling coordinationNoYes
GPU constraintsNoYes
Helm chart supportYesRequires forked chart

Why Not DRA Now? The official Plex Helm chart doesn't support DRA because it lacks:

  • extraPodSpec for pod-level resourceClaims
  • extraContainerSpec for container-level resources.claims

DRA requires both:

spec:
  template:
    spec:
      resourceClaims:           # Pod level
        - name: gpu
          resourceClaimTemplateName: intel-gpu
      containers:
        - resources:
            claims:             # Container level - chart doesn't expose this
              - name: gpu

Future: Forked Chart Approach. To enable DRA, fork the Plex chart and add:

  1. values.yaml - Add dra.enabled, dra.resourceClaimTemplateName
  2. statefulset.yaml - Add conditional resourceClaims blocks
  3. GitHub Pages - Host forked chart as Helm repo

Then reference the forked chart in HelmRelease with DRA enabled. This provides proper Kubernetes-native GPU allocation via ResourceClaims.

Bulk Media via NFS: For larger media libraries, NFS from a dedicated NAS is recommended:

  • Talos has no SSH - can't rsync directly to nodes
  • Longhorn PVCs aren't accessible as directories outside pods
  • NFS mountable from any device (Mac, cluster pods, personal devices)
  • Separates compute (cluster) from storage (NAS)

This article uses Longhorn for initial testing. NFS migration is a future enhancement.

Verify GPU

Before deploying Plex, confirm GPUs are available on nodes.

DRI (Direct Rendering Infrastructure)

talosctl -n 192.168.1.30 ls /dev/dri

Expected: card0 and renderD128 devices.

Create Plex Manifests

Namespace

k8s/apps/plex/namespace.yaml:

---
apiVersion: v1
kind: Namespace
metadata:
  name: plex
  labels:
    pod-security.kubernetes.io/enforce: privileged
    pod-security.kubernetes.io/audit: privileged
    pod-security.kubernetes.io/warn: privileged
Note:Plex needs privileged access for GPU hostPath mount.

PersistentVolumeClaims

Longhorn storage for Plex config and media.

k8s/apps/plex/pvc.yaml:

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: plex-config
  namespace: plex
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 50Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: plex-media
  namespace: plex
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 500Gi
Note:Adjust plex-media size based on your storage needs. The 2TB NVMe provides ample room.

Claim Token Secret

The Plex claim token links your server to your Plex account.

k8s/apps/plex/secret.sops.yaml (before encryption):

---
apiVersion: v1
kind: Secret
metadata:
  name: plex-claim
  namespace: plex
type: Opaque
stringData:
  claim: "claim-XXXXXXXX"  # Replace with your token from https://www.plex.tv/claim/
Important:Claim tokens expire in 4 minutes. Generate one right before encrypting.

Encrypt Secret

sops --encrypt --in-place k8s/apps/plex/secret.sops.yaml

After initial Plex setup completes, you can delete this secret (the claim is only used once).

HelmRelease

Using the official Plex chart3 with GPU access via hostPath. The claim token is injected from the SOPS secret via valuesFrom.

k8s/apps/plex/helmrelease.yaml:

---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: plex
  namespace: plex
spec:
  interval: 24h
  url: https://raw.githubusercontent.com/plexinc/pms-docker/gh-pages
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: plex
  namespace: plex
spec:
  interval: 30m
  chart:
    spec:
      chart: plex-media-server
      version: "0.x"
      sourceRef:
        kind: HelmRepository
        name: plex
  valuesFrom:
    - kind: Secret
      name: plex-claim
      valuesKey: claim
      targetPath: extraEnv.PLEX_CLAIM
  values:
    extraEnv:
      TZ: "America/Denver"

    # Service - MetalLB assigns IP
    service:
      type: LoadBalancer
      port: 32400

    # Config storage
    pms:
      configExistingClaim: plex-config

    # Media and GPU volumes
    extraVolumes:
      - name: media
        persistentVolumeClaim:
          claimName: plex-media
      - name: dev-dri
        hostPath:
          path: /dev/dri
          type: Directory

    extraVolumeMounts:
      - name: media
        mountPath: /data/media
      - name: dev-dri
        mountPath: /dev/dri
Claim token:Injected from the SOPS-encrypted secret via valuesFrom. Flux decrypts the secret automatically.
GPU access:The /dev/dri hostPath mount exposes Intel GPU devices to the container for hardware transcoding.

Kustomization

k8s/apps/plex/kustomization.yaml:

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - namespace.yaml
  - pvc.yaml
  - secret.sops.yaml
  - helmrelease.yaml

Apps Kustomization

Add plex to k8s/apps/kustomization.yaml:

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - plex

Deploy Plex

Commit Changes

cd ~/homelab

# Get claim token NOW (4 minute window)
# Visit: https://www.plex.tv/claim/
# Update k8s/apps/plex/secret.sops.yaml with the token

git add k8s/apps/
git commit -m "feat(plex): add Plex with GPU transcoding"
git push

Reconcile Flux

flux reconcile source git flux-system
flux reconcile kustomization sync

Verify Deployment

Plex Resources

# Check HelmRelease status
flux get helmreleases -n plex

# Check pods
kubectl get pods -n plex

# Check service (note the EXTERNAL-IP)
kubectl get svc -n plex

# View logs
kubectl logs -n plex -l app.kubernetes.io/name=plex-media-server -f

Create Media Directories

Create the library directories before accessing Plex:

kubectl exec -n plex -it plex-plex-media-server-0 -- mkdir -p /data/media/movies /data/media/tv /data/media/music

These persist in the Longhorn PVC, so this only needs to be done once.

Access Plex

Get the LoadBalancer IP:

kubectl get svc -n plex plex-plex-media-server -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

Access at: http://<EXTERNAL-IP>:32400/web

Configure Plex

Initial Setup

  1. Sign in with your Plex account
  2. Name your server: e.g., "Homelab"
  3. Add libraries:
    • Movies: /data/media/movies
    • TV Shows: /data/media/tv
    • Music: /data/media/music
  4. Verify hardware transcoding (enabled by default):
    • Settings → Transcoder → "Use hardware acceleration when available" should be checked

Verify Hardware Transcoding

Hardware transcoding4 offloads video encoding/decoding to the GPU.

GPU Access

kubectl exec -n plex -it plex-plex-media-server-0 -- ls -la /dev/dri

Expected:

card0
renderD128

Transcoding

  1. Play a video in Plex
  2. Change quality to force transcode (e.g., 720p if source is 4K)
  3. Check dashboard - should show "(hw)" indicator for hardware transcoding

From CLI:

kubectl logs -n plex -l app.kubernetes.io/name=plex-media-server | grep -i transcode

Add Media

Copy via kubectl

For initial testing, copy media directly via kubectl:

# Copy a file
kubectl cp /path/to/movie.mkv plex/plex-plex-media-server-0:/data/media/movies/

This works for small files but is slow for bulk media. See NFS approach in What's Not In Scope.

Next Steps

With Plex running, deploy more services.

See: Factorio Kubernetes Server

Resources

Footnotes

  1. Reddit, "Plex on Kubernetes with Intel iGPU passthrough," reddit.com. Accessed: Dec. 16, 2025. [Online]. Available: https://www.reddit.com/r/selfhosted/comments/121vb07/plex_on_kubernetes_with_intel_igpu_passthrough/

  2. Kubernetes, "Dynamic Resource Allocation," kubernetes.io. Accessed: Dec. 16, 2025. [Online]. Available: https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/

  3. Plex, "Plex Media Server Docker," github.com. Accessed: Dec. 16, 2025. [Online]. Available: https://github.com/plexinc/pms-docker

  4. Plex, "Using Hardware-Accelerated Streaming," support.plex.tv. Accessed: Dec. 16, 2025. [Online]. Available: https://support.plex.tv/articles/115002178853-using-hardware-accelerated-streaming/

Previous
Intel Arc Kubernetes DRA