Publish Plex chart and replace direct Kubernetes CD
Validate, publish and promote Plex chart / publish (push) Successful in 2s
Validate, publish and promote Plex chart / validate (push) Successful in 1s

This commit is contained in:
feedc0de committed 2026-10-23 09:48:55 +02:00
1 parent 4d2bff4fac
commit 1d84fb0bc4
9 files changed
+179 -147

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: plex
description: Plex Media Server with guarded host-local media and optional monitoring
type: application
version: 0.0.0
appVersion: "1.43.4.10903-e5521bd8c"
+388
View File
@@ -0,0 +1,388 @@
{{- if not .Values.persistence.existingClaim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: plex-config
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: state
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . }}
{{- end }}
{{- end }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: plex-scripts
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: plex
data:
check-media.sh: |
#!/bin/sh
set -eu
filesystem_magic=$(stat -f -c '%t' {{ .Values.media.mountPath }})
if [ "${filesystem_magic}" != ca451a4e ]; then
echo "Expected bcachefs at {{ .Values.media.mountPath }}, found filesystem magic ${filesystem_magic}" >&2
exit 1
fi
test -d {{ .Values.media.mountPath }}/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: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: server
annotations:
deployment.brunner.ninja/revision: manual
spec:
replicas: 1
progressDeadlineSeconds: 3900
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:
{{- toYaml .Values.nodeSelector | nindent 8 }}
tolerations:
{{- toYaml .Values.tolerations | nindent 8 }}
terminationGracePeriodSeconds: 120
securityContext:
seccompProfile:
type: RuntimeDefault
initContainers:
- name: media-ready
image: docker.io/library/alpine:3.24.2
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: {{ .Values.media.mountPath }}
readOnly: true
- name: media-ready
mountPath: /media-ready
readOnly: true
- name: tools
mountPath: /tools
readOnly: true
- name: exporter-token
image: docker.io/library/alpine:3.24.2
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: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: {{ .Values.timezone | quote }}
- name: PLEX_UID
value: {{ .Values.plexUid | quote }}
- name: PLEX_GID
value: {{ .Values.plexGid | quote }}
- name: CHANGE_CONFIG_DIR_OWNERSHIP
value: "false"
ports:
- name: http
containerPort: 32400
protocol: TCP
startupProbe:
httpGet:
path: /identity
port: http
failureThreshold: 720
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:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
volumeMounts:
- name: config
mountPath: /config
- name: media
mountPath: {{ .Values.media.mountPath }}
readOnly: true
- name: transcode
mountPath: /transcode
- name: plex-exporter
image: ghcr.io/cplieger/plex-exporter:v4.4.0
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: {{ default "plex-config" .Values.persistence.existingClaim }}
- name: media
hostPath:
path: {{ .Values.media.hostPath }}
type: Directory
- name: media-ready
hostPath:
path: {{ .Values.media.hostPath }}/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: {{ .Release.Namespace }}
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
{{- if .Values.ingress.enabled }}
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: {{ default "plex" .Values.ingress.name }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: plex
spec:
entryPoints:
- {{ .Values.ingress.entryPoint }}
routes:
- kind: Rule
match: Host(`{{ required "ingress.host is required" .Values.ingress.host }}`)
services:
- kind: Service
name: plex
port: http
passHostHeader: true
{{- end }}
{{- if .Values.monitoring.enabled }}
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: plex
namespace: {{ .Release.Namespace }}
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: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: plex
spec:
groups:
- name: plex.availability
rules:
- alert: PlexMetricsTargetDown
expr: |-
max(kube_deployment_status_replicas_available{namespace="{{ .Release.Namespace }}",deployment="plex"}) >= 1
and on()
(
max(up{namespace="{{ .Release.Namespace }}",service="plex"}) < 1
or absent(up{namespace="{{ .Release.Namespace }}",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="{{ .Release.Namespace }}",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="{{ .Release.Namespace }}",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="{{ .Release.Namespace }}",persistentvolumeclaim="{{ default "plex-config" .Values.persistence.existingClaim }}"}
/
kubelet_volume_stats_capacity_bytes{namespace="{{ .Release.Namespace }}",persistentvolumeclaim="{{ default "plex-config" .Values.persistence.existingClaim }}"} < 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.
{{- end }}
+23
View File
@@ -0,0 +1,23 @@
image:
repository: docker.io/plexinc/pms-docker
tag: ""
media:
hostPath: /media
mountPath: /media
nodeSelector: {}
tolerations: []
persistence:
existingClaim: ""
storageClass: ""
size: 160Gi
timezone: UTC
plexUid: "964"
plexGid: "964"
resources: {}
ingress:
enabled: false
name: ""
host: ""
entryPoint: websecure
monitoring:
enabled: false