Deploying Plex to a Kubernetes cluster is much the same as deploying any old Docker image to a cluster. There are two images (1, 2) that one can use that simply wrap Plex; I haven't seen much difference between them. There is another project that splits transcoding across pods called kube-plex also. I use plex-inc/pms-docker for simplicity. If you decide to use it as well, please refer to the pms-docker repo for the most up to date configuration options.
Below is my configuration for the Plex deployment.
apiVersion: apps/v1 kind: Deployment metadata: name: plex-media-server labels: app: plex-media-server spec: selector: matchLabels: app: plex-media-server template: metadata: labels: app: plex-media-server spec: containers: # You should probably pin this to a specific version - image: plexinc/pms-docker:latest name: plex-media-server env: - name: TZ value: "US/Eastern" # Only needed during first run; https://www.plex.tv/claim/ - name: PLEX_CLAIM value: "claim-someclaimtoken" - name: ADVERTISE_IP value: "https://plex.local:32400" - name: CHANGE_CONFIG_DIR_OWNERSHIP value: "false" volumeMounts: # This is the media share - mountPath: /data name: plex-share # Transcoding cache - mountPath: /transcode name: plex-transcode # Persistent config, logs, some caching - mountPath: /config name: scsi-plex-config restartPolicy: Always volumes: - name: plex-share persistentVolumeClaim: claimName: plex-share - name: scsi-plex-config persistentVolumeClaim: claimName: scsi-plex-config # Storing transcode data on the node is fine for me # Make sure you have the space for this on the worker - name: plex-transcode hostPath: path: "/mnt/plex-transcode" --- apiVersion: v1 kind: Service metadata: name: plex-media-server spec: selector: app: plex-media-server type: NodePort ports: - name: plex port: 32400 nodePort: 32400
Note: I don't actually use plex.local
but you could feasibly configure a home DNS
resolver to resolve plex.local
to a node in the cluster (because NodePort is a
good load balancer itself) or possibly a discrete load balancer in front of the
cluster. Nginx has been good to me in this capacity, and as an SSL termination
point.
Now for the quirky bit. To serve these PersistentVolumes to Kubernetes I originally employed NFS for both the shared media and config. Plex uses SQLite for its database. If you know the relationship between NFS and SQLite you know where this is going.
As I added media to the server I noticed it took a non-trivial amount of time to
process new files. Considering that CPU usage did not spike significantly I knew
something was misconfigured. When Plex started randomly losing connectivity and
seemingly freezing I tailed the logs at /config/Library/Application
Support/Plex Media Server/Logs/Plex Media Server.log
and observed the
following messages:
DEBUG - 23 threads are waiting on db connections held by threads
Hmm. I dug into this and learned about the problems SQLite has running over NFS. Instead of serving the config over NFS I settled on serving it over iSCSI. I allocated an LVM logical volume and added this as a block device served over iSCSI:
[root@dullscythe:/]# vcreate --size 100G --name kube-scsi-plex vg0 [root@dullscythe:/]# nix-shell -p targetcli --run 'targetcli' targetcli shell version 2.1.57 Copyright 2011-2013 by Datera, Inc and others. For help on commands, type 'help'. /> /iscsi create Created target iqn.2003-01.org.linux-iscsi.dullscythe.x8664:sn.938cb8d24788. Created TPG 1. Global pref auto_add_default_portal=true Created default portal listening on all IPs (0.0.0.0), port 3260. /> /backstores/block create kube_scsi_plex /dev/vg0/kube-scsi-plex Created block storage object kube_scsi_plex using /dev/vg0/kube-scsi-plex. /> /iscsi/iqn.2003-01.org.linux-iscsi.dullscythe.x8664:sn.938cb8d24788/tpg1/luns create /backstores/block/kube_scsi_plex Created LUN 0. /> /iscsi/iqn.2003-01.org.linux-iscsi.dullscythe.x8664:sn.938cb8d24788/tpg1/acls create iqn.2003-01.org.linux-iscsi.dullscythe.client:kubes-plex Created Node ACL for iqn.2003-01.org.linux-iscsi.dullscythe.client:kubes-plex Created mapped LUN 0. /> /iscsi/iqn.2003-01.org.linux-iscsi.dullscythe.x8664:sn.938cb8d24788/tpg1/ set attribute authentication=0 Parameter authentication is now '0'. /> saveconfig Last 10 configs saved in /etc/target/backup/. Configuration saved to /etc/target/saveconfig.json
Note that targetcli interacts with the kernel and does not save its config by
default; running saveconfig
is needed to persist changes through a reboot.
Now there is an iSCSI target available. Firewall rules can help to isolate
this drive from the rest of your network as it is presently listening on
0.0.0.0:3260 and not guarded with auth. You could optionally configure CHAP
authentication, which I disabled with set attribute authentication=0
.
Enable the target
service (services.target.enable = true
in NixOS ^^),
install open-iscsi
on all workers that may host this pod and finally start and
enable iscsid
on the workers.
The spec that provides storage to the Plex deployment is below:
apiVersion: v1 kind: PersistentVolume metadata: name: plex-share spec: storageClassName: plex-share capacity: # Yours will be a different size storage: 10Ti volumeMode: Filesystem accessModes: - ReadOnlyMany persistentVolumeReclaimPolicy: Retain mountOptions: - hard - nfsvers=4.2 nfs: # ... or however you access your media share over a network path: "/public" server: 10.0.30.1 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: plex-share spec: storageClassName: plex-share accessModes: - ReadOnlyMany resources: requests: # Yours will be a different size storage: 10Ti --- apiVersion: v1 kind: PersistentVolume metadata: name: scsi-plex-config spec: storageClassName: scsi-plex-config capacity: storage: 100Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain iscsi: targetPortal: 10.0.30.1:3260 # Use the IQN generated during the above steps iqn: iqn.2003-01.org.linux-iscsi.dullscythe.x8664:sn.938cb8d24788 lun: 0 fsType: ext4 readOnly: false chapAuthDiscovery: false chapAuthSession: false # Use the initiator name configured during the ACL step above initiatorName: iqn.2003-01.org.linux-iscsi.dullscythe.client:kubes-plex --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: scsi-plex-config spec: storageClassName: scsi-plex-config accessModes: - ReadWriteOnce resources: requests: storage: 100Gi
I magnanimously present this article gratis as it took a good amount of digging to pinpoint the cause of this issue. For setting up a bare-metal Kubernetes cluster check out my companion article, On-Prem Home Lab Kubernetes.