apiVersion: v1 kind: PersistentVolumeClaim metadata: name: plex-config namespace: plex labels: app.kubernetes.io/name: plex app.kubernetes.io/component: state spec: accessModes: - ReadWriteOnce resources: requests: storage: 160Gi storageClassName: rook-ceph-block-ec --- apiVersion: v1 kind: ConfigMap metadata: name: plex-scripts namespace: plex labels: app.kubernetes.io/name: plex data: check-media.sh: | #!/bin/sh set -eu filesystem_magic=$(stat -f -c '%t' /komposthaufen/multimedia) if [ "${filesystem_magic}" != ca451a4e ]; then echo "Expected bcachefs at /komposthaufen/multimedia, found filesystem magic ${filesystem_magic}" >&2 exit 1 fi test -d /komposthaufen/multimedia/Videos extract-token.sh: | #!/bin/sh set -eu preferences='/config/Library/Application Support/Plex Media Server/Preferences.xml' test -s "${preferences}" token=$(sed -n 's/.*PlexOnlineToken="\([^"]*\)".*/\1/p' "${preferences}") if [ -z "${token}" ]; then echo 'PlexOnlineToken is missing from Preferences.xml' >&2 exit 1 fi umask 077 printf '%s' "${token}" > /token/plex-token --- apiVersion: apps/v1 kind: Deployment metadata: name: plex namespace: plex labels: app.kubernetes.io/name: plex app.kubernetes.io/component: server annotations: deployment.brunner.ninja/revision: manual spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: plex app.kubernetes.io/component: server template: metadata: labels: app.kubernetes.io/name: plex app.kubernetes.io/component: server spec: automountServiceAccountToken: false hostNetwork: true dnsPolicy: ClusterFirstWithHostNet nodeSelector: kubernetes.io/hostname: arschrock media.brunner.ninja/multimedia-ready: "true" tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule terminationGracePeriodSeconds: 120 securityContext: seccompProfile: type: RuntimeDefault initContainers: - name: media-ready image: docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b imagePullPolicy: IfNotPresent command: - /tools/check-media.sh securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65534 runAsGroup: 65534 volumeMounts: - name: media mountPath: /komposthaufen/multimedia readOnly: true - name: media-ready mountPath: /media-ready readOnly: true - name: tools mountPath: /tools readOnly: true - name: exporter-token image: docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b imagePullPolicy: IfNotPresent command: - /tools/extract-token.sh securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 964 runAsGroup: 964 volumeMounts: - name: config mountPath: /config readOnly: true - name: exporter-token mountPath: /token - name: tools mountPath: /tools readOnly: true containers: - name: plex image: docker.io/plexinc/pms-docker@sha256:f6748983db1054b571b57b4a40f07f53af6c4bfb9edd1fa455f5ebb6e16449bc imagePullPolicy: IfNotPresent env: - name: TZ value: Europe/Vienna - name: PLEX_UID value: "964" - name: PLEX_GID value: "964" - name: CHANGE_CONFIG_DIR_OWNERSHIP value: "false" ports: - name: http containerPort: 32400 protocol: TCP startupProbe: httpGet: path: /identity port: http failureThreshold: 180 periodSeconds: 5 timeoutSeconds: 3 readinessProbe: httpGet: path: /identity port: http failureThreshold: 3 periodSeconds: 10 timeoutSeconds: 3 livenessProbe: httpGet: path: /identity port: http failureThreshold: 3 periodSeconds: 30 timeoutSeconds: 5 resources: requests: cpu: 500m memory: 2Gi ephemeral-storage: 1Gi securityContext: allowPrivilegeEscalation: false volumeMounts: - name: config mountPath: /config - name: media mountPath: /komposthaufen/multimedia readOnly: true - name: transcode mountPath: /transcode - name: plex-exporter image: ghcr.io/cplieger/plex-exporter@sha256:07ee6d213698bfb0dc086004f26e1048380caaa229f0429e9aafa2eb487ee826 imagePullPolicy: IfNotPresent env: - name: PLEX_URL value: http://127.0.0.1:32400 - name: PLEX_TOKEN_FILE value: /token/plex-token ports: - name: metrics containerPort: 9594 protocol: TCP startupProbe: httpGet: path: /api/health port: metrics failureThreshold: 60 periodSeconds: 5 timeoutSeconds: 3 readinessProbe: httpGet: path: /api/health port: metrics failureThreshold: 3 periodSeconds: 10 timeoutSeconds: 3 livenessProbe: httpGet: path: /api/health port: metrics failureThreshold: 3 periodSeconds: 30 timeoutSeconds: 5 resources: requests: cpu: 10m memory: 32Mi securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 964 runAsGroup: 964 volumeMounts: - name: exporter-token mountPath: /token readOnly: true - name: exporter-tmp mountPath: /tmp volumes: - name: config persistentVolumeClaim: claimName: plex-config - name: media hostPath: path: /komposthaufen/multimedia type: Directory - name: media-ready hostPath: path: /komposthaufen/multimedia/Videos type: Directory - name: transcode emptyDir: {} - name: exporter-token emptyDir: medium: Memory sizeLimit: 1Mi - name: exporter-tmp emptyDir: medium: Memory sizeLimit: 8Mi - name: tools configMap: name: plex-scripts defaultMode: 0555 --- apiVersion: v1 kind: Service metadata: name: plex namespace: plex labels: app.kubernetes.io/name: plex app.kubernetes.io/component: service spec: selector: app.kubernetes.io/name: plex app.kubernetes.io/component: server ports: - name: http port: 32400 targetPort: http protocol: TCP - name: metrics port: 9594 targetPort: metrics protocol: TCP --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: plex-brunner-ninja namespace: plex labels: app.kubernetes.io/name: plex spec: entryPoints: - websecure routes: - kind: Rule match: Host(`plex.brunner.ninja`) services: - kind: Service name: plex port: http passHostHeader: true --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: plex namespace: plex labels: app.kubernetes.io/name: plex spec: selector: matchLabels: app.kubernetes.io/name: plex app.kubernetes.io/component: service endpoints: - port: metrics path: /metrics interval: 30s scrapeTimeout: 10s metricRelabelings: - action: labeldrop regex: (pod|endpoint|container) --- apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: plex namespace: plex labels: app.kubernetes.io/name: plex spec: groups: - name: plex.availability rules: - alert: PlexMetricsTargetDown expr: |- max(kube_deployment_status_replicas_available{namespace="plex",deployment="plex"}) >= 1 and on() ( max(up{namespace="plex",service="plex"}) < 1 or absent(up{namespace="plex",service="plex"}) ) for: 10m labels: severity: warning component: exporter annotations: summary: Plex metrics are unavailable description: The Plex Pod is available, but Prometheus cannot scrape its exporter. - alert: PlexAPIUnreachable expr: max(plex_http_reachable{namespace="plex",service="plex"}) < 1 for: 10m labels: severity: warning component: plex annotations: summary: Plex API is unreachable from its exporter description: The exporter has failed to poll Plex for at least ten minutes. - alert: PlexContainerRestarting expr: |- sum by (container) ( increase(kube_pod_container_status_restarts_total{namespace="plex",pod=~"plex-.*"}[15m]) ) > 2 for: 5m labels: severity: warning component: kubernetes annotations: summary: Plex container is repeatedly restarting description: Container {{ $labels.container }} restarted more than twice in 15 minutes. - alert: PlexConfigVolumeFilling expr: |- kubelet_volume_stats_available_bytes{namespace="plex",persistentvolumeclaim="plex-config"} / kubelet_volume_stats_capacity_bytes{namespace="plex",persistentvolumeclaim="plex-config"} < 0.15 for: 30m labels: severity: warning component: storage annotations: summary: Plex configuration volume is filling description: Less than 15 percent of the Plex configuration PVC remains available.