Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f89e1c8260 | |||
| ed75a6d1e1 | |||
| 5a4bc1611e | |||
| c0c3eb4f66 | |||
| a8d52311cd |
@@ -36,7 +36,7 @@ spec:
|
||||
mountPath: /combined-certs
|
||||
containers:
|
||||
- name: api
|
||||
image: git.unkin.net/unkin/artifactapi:v3.7.7
|
||||
image: git.unkin.net/unkin/artifactapi:v3.8.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
automountServiceAccountToken: true
|
||||
containers:
|
||||
- name: ui
|
||||
image: git.unkin.net/unkin/artifactapi-ui:v3.7.7
|
||||
image: git.unkin.net/unkin/artifactapi-ui:v3.8.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
# Ceph RGW (S3) backup target for the jellyfin CNPG cluster, provisioned by the
|
||||
# in-estate cephrgw-operator: one dedicated bucket + owner user. CNPG reads the
|
||||
# S3 credential Secret from its own namespace.
|
||||
apiVersion: ceph.unkin.net/v1alpha1
|
||||
kind: ObjectStoreUser
|
||||
metadata:
|
||||
name: cnpg-jellyfin-backup
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
displayName: "CNPG backup owner (jellyfin)"
|
||||
uid: cnpg-jellyfin-backup
|
||||
maxBuckets: 5
|
||||
secretName: cnpg-jellyfin-backup-s3
|
||||
retainOnDelete: true
|
||||
---
|
||||
apiVersion: ceph.unkin.net/v1alpha1
|
||||
kind: Bucket
|
||||
metadata:
|
||||
name: cnpg-jellyfin
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
placementTarget: ec
|
||||
bucketName: cnpg-jellyfin
|
||||
ownerRef: cnpg-jellyfin-backup
|
||||
versioning: false
|
||||
tags:
|
||||
app: jellyfin
|
||||
purpose: cnpg-backup
|
||||
retainOnDelete: true
|
||||
---
|
||||
# Nightly base backup on top of always-on WAL archiving. Scheduled off-peak and
|
||||
# staggered from the other CNPG clusters (6-field cron, seconds first).
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: ScheduledBackup
|
||||
metadata:
|
||||
name: cnpg-jellyfin-nightly
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
schedule: "0 35 3 * * *"
|
||||
immediate: false
|
||||
backupOwnerReference: self
|
||||
method: barmanObjectStore
|
||||
cluster:
|
||||
name: jellyfin-postgres
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
# Main Jellyfin database. The jellyfin-ha fork's experimental EF Core provider
|
||||
# moves the entire Jellyfin DB (incl. library items) off SQLite into PostgreSQL,
|
||||
# which is what makes a shared-nothing multi-replica deployment possible. No
|
||||
# bootstrap secret is given, so CNPG generates the jellyfin-postgres-app secret
|
||||
# (username/password/dbname) that the StatefulSet composes its DSN from.
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: jellyfin-postgres
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinityType: preferred
|
||||
backup:
|
||||
retentionPolicy: 30d
|
||||
barmanObjectStore:
|
||||
# Dedicated per-cluster Ceph RGW bucket (cephrgw-operator provisions it).
|
||||
destinationPath: s3://cnpg-jellyfin
|
||||
endpointURL: https://s3.ceph.unkin.net
|
||||
endpointCA:
|
||||
name: vault-ca-cert
|
||||
key: ca.crt
|
||||
s3Credentials:
|
||||
accessKeyId:
|
||||
name: cnpg-jellyfin-backup-s3
|
||||
key: AWS_ACCESS_KEY_ID
|
||||
secretAccessKey:
|
||||
name: cnpg-jellyfin-backup-s3
|
||||
key: AWS_SECRET_ACCESS_KEY
|
||||
serverName: jellyfin
|
||||
data:
|
||||
compression: bzip2
|
||||
jobs: 2
|
||||
wal:
|
||||
compression: zstd
|
||||
maxParallel: 2
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: jellyfin
|
||||
encoding: UTF8
|
||||
localeCType: C
|
||||
localeCollate: C
|
||||
owner: jellyfin
|
||||
enablePDB: true
|
||||
enableSuperuserAccess: false
|
||||
failoverDelay: 0
|
||||
# PG 17 — accepted by the fork's Npgsql/EF Core provider (needs PG14+); the
|
||||
# provider generates its own migrations on first start.
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:17-system-trixie
|
||||
instances: 3
|
||||
logLevel: info
|
||||
maxSyncReplicas: 0
|
||||
minSyncReplicas: 0
|
||||
monitoring:
|
||||
customQueriesConfigMap:
|
||||
- key: queries
|
||||
name: cnpg-default-monitoring
|
||||
disableDefaultQueries: false
|
||||
enablePodMonitor: false
|
||||
postgresql:
|
||||
parameters:
|
||||
archive_mode: "on"
|
||||
archive_timeout: 5min
|
||||
dynamic_shared_memory_type: posix
|
||||
effective_cache_size: 256MB
|
||||
full_page_writes: "on"
|
||||
log_destination: csvlog
|
||||
log_directory: /controller/log
|
||||
log_filename: postgres
|
||||
log_rotation_age: "0"
|
||||
log_rotation_size: "0"
|
||||
log_truncate_on_rotation: "false"
|
||||
logging_collector: "on"
|
||||
max_connections: "200"
|
||||
max_parallel_workers: "16"
|
||||
max_replication_slots: "16"
|
||||
max_worker_processes: "16"
|
||||
shared_buffers: 128MB
|
||||
shared_memory_type: mmap
|
||||
ssl_max_protocol_version: TLSv1.3
|
||||
ssl_min_protocol_version: TLSv1.3
|
||||
wal_keep_size: 256MB
|
||||
wal_level: logical
|
||||
wal_log_hints: "on"
|
||||
wal_receiver_timeout: 5s
|
||||
wal_sender_timeout: 5s
|
||||
syncReplicaElectionConstraint:
|
||||
enabled: false
|
||||
primaryUpdateMethod: restart
|
||||
primaryUpdateStrategy: unsupervised
|
||||
probes:
|
||||
liveness:
|
||||
isolationCheck:
|
||||
connectionTimeout: 1000
|
||||
enabled: true
|
||||
requestTimeout: 1000
|
||||
replicationSlots:
|
||||
highAvailability:
|
||||
enabled: true
|
||||
slotPrefix: _cnpg_
|
||||
synchronizeReplicas:
|
||||
enabled: true
|
||||
updateInterval: 30
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 512Mi
|
||||
smartShutdownTimeout: 180
|
||||
startDelay: 3600
|
||||
stopDelay: 1800
|
||||
storage:
|
||||
resizeInUseVolumes: true
|
||||
size: 10Gi
|
||||
storageClass: cephrbd-fast-delete
|
||||
switchoverDelay: 3600
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
# PgBouncer pooler in front of the jellyfin-postgres cluster. Jellyfin connects
|
||||
# here (jellyfin-postgres-pooler:5432) rather than the -rw service so EF Core's
|
||||
# connection churn is absorbed by the pool.
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Pooler
|
||||
metadata:
|
||||
name: jellyfin-postgres-pooler
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
cluster:
|
||||
name: jellyfin-postgres
|
||||
instances: 2
|
||||
pgbouncer:
|
||||
parameters:
|
||||
default_pool_size: "50"
|
||||
max_client_conn: "200"
|
||||
paused: false
|
||||
poolMode: session
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jellyfin-pooler
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- jellyfin-pooler
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers: []
|
||||
type: rw
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
# Single-replica for now. The jellyfin-ha fork adds the Redis transcode store
|
||||
# and RWX transcode scratch that make scaling to true HA a follow-up.
|
||||
replicas: 1
|
||||
strategy:
|
||||
# Config PVC is RWO; Recreate avoids two pods contending for it.
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jellyfin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jellyfin
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
nodeSelector:
|
||||
feature.node.kubernetes.io/pci-0300_8086.present: "true"
|
||||
containers:
|
||||
- name: jellyfin
|
||||
image: git.unkin.net/unkin/jellyfin-ha:v0.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8096
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: TZ
|
||||
value: Australia/Sydney
|
||||
- name: PUID
|
||||
value: "1000"
|
||||
- name: PGID
|
||||
value: "1000"
|
||||
- name: JELLYFIN_PublishedServerUrl
|
||||
value: https://jellyfin.k8s.syd1.au.unkin.net
|
||||
# Distributed transcode session store (jellyfin-ha additions).
|
||||
- name: Jellyfin__TranscodeStore__RedisConnectionString
|
||||
value: "jellyfin-redis:6379,abortConnect=false"
|
||||
- name: Jellyfin__TranscodeStore__LeaseDurationSeconds
|
||||
value: "30"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: 8Gi
|
||||
gpu.intel.com/i915: "1"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: cache
|
||||
mountPath: /cache
|
||||
- name: transcode
|
||||
mountPath: /transcode
|
||||
- name: media-library
|
||||
mountPath: /mnt/movies
|
||||
subPath: movies
|
||||
- name: media-library
|
||||
mountPath: /mnt/tvseries
|
||||
subPath: tvseries
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-config
|
||||
- name: cache
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-cache
|
||||
- name: transcode
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-transcode
|
||||
- name: media-library
|
||||
persistentVolumeClaim:
|
||||
claimName: media-library
|
||||
@@ -26,7 +26,7 @@ spec:
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: jellyfin
|
||||
name: jellyfin-route
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
hostnames:
|
||||
|
||||
@@ -4,15 +4,17 @@ kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- pv_media-library.yaml
|
||||
- pvc_media-library.yaml
|
||||
- pvc_config.yaml
|
||||
- pvc_cache.yaml
|
||||
- pvc_transcode.yaml
|
||||
- deployment.yaml
|
||||
- cnpg_cluster.yaml
|
||||
- cnpg_pooler.yaml
|
||||
- cnpg_backup.yaml
|
||||
- pvc-config.yaml
|
||||
- pvc-transcode.yaml
|
||||
- pvc-media.yaml
|
||||
- statefulset.yaml
|
||||
- pdb.yaml
|
||||
- service.yaml
|
||||
- redis-deployment.yaml
|
||||
- redis-service.yaml
|
||||
- redis-pvc.yaml
|
||||
- redis-service.yaml
|
||||
- gateway.yaml
|
||||
- httproute.yaml
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# Keep at least one Jellyfin replica serving through voluntary disruptions
|
||||
# (node drains, rollouts) so active streams can fail over rather than drop.
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jellyfin
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
# Static CephFS PersistentVolume bound to the pre-existing, ACTIVELY-USED
|
||||
# puppet media library (ceph filesystem "mediafs", mounted by the VM/incus
|
||||
# instances at /shared/media). ceph-csi only mounts this volume; staticVolume
|
||||
# tells it the storage pre-exists and it must never provision or delete it.
|
||||
#
|
||||
# reclaimPolicy MUST stay Retain: deleting this PV or its PVC must NEVER be able
|
||||
# to reclaim or destroy the underlying CephFS data that the VM instances use.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: jellyfin-media-library
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
capacity:
|
||||
storage: 10Ti
|
||||
# Load-bearing safety control. Do not change to Delete.
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: ""
|
||||
volumeMode: Filesystem
|
||||
# Pre-bind to the media-library claim so nothing else can grab this PV.
|
||||
claimRef:
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
name: media-library
|
||||
namespace: jellyfin
|
||||
csi:
|
||||
driver: cephfs.csi.ceph.com
|
||||
volumeHandle: jellyfin-media-library-static
|
||||
nodeStageSecretRef:
|
||||
name: csi-cephfs-secret
|
||||
namespace: csi-cephfs
|
||||
volumeAttributes:
|
||||
# clusterID maps (in the csi-cephfs ceph-csi-config) to the mon set that
|
||||
# also serves mediafs; for a static volume only the mon lookup is used.
|
||||
clusterID: cephfs_csi_ssd_ec_4_1
|
||||
fsName: mediafs
|
||||
staticVolume: "true"
|
||||
# Filesystem-internal root of the library (mediafs root == /shared/media).
|
||||
rootPath: /
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
# Jellyfin config: metadata images, plugins, subtitles and config XML. Shared
|
||||
# ReadWriteMany across replicas (all pods read/write the same library metadata);
|
||||
# the main library DB now lives in PostgreSQL, not here. Retain — this is state.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jellyfin-config
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
storageClassName: cephfs-raid5-retain
|
||||
volumeMode: Filesystem
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
# Media library, shared read-many across replicas. Retain — this holds the
|
||||
# actual media and must survive PVC deletion. Empty on first deploy; populating
|
||||
# it is out of scope for this app.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jellyfin-media
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Ti
|
||||
storageClassName: cephfs-raid6-retain
|
||||
volumeMode: Filesystem
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
# Shared transcode scratch. ReadWriteMany is the hard requirement for the HA
|
||||
# fork: a taking-over pod must read the in-flight HLS segments written by the
|
||||
# pod it replaces. Scratch data, so delete reclaim policy.
|
||||
# pod it replaces. Scratch data (delete reclaim); raid5 avoids the raid6
|
||||
# double-parity write penalty on the many small HLS segment writes.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
# Local transcode/image cache. Scratch, delete reclaim policy.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jellyfin-cache
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 200Gi
|
||||
storageClassName: cephrbd-fast-delete
|
||||
volumeMode: Filesystem
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
# Jellyfin config + SQLite library database. Single-writer, block storage.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jellyfin-config
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
storageClassName: cephrbd-fast-retain
|
||||
volumeMode: Filesystem
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
# Claim bound to the static mediafs PV. RWX so every app in the stack shares the
|
||||
# one library. storageClassName "" + volumeName pin it to the static PV (no
|
||||
# dynamic provisioning). Deleting this claim cannot reclaim the data (PV is
|
||||
# Retain).
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: media-library
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: ""
|
||||
volumeName: jellyfin-media-library
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Ti
|
||||
volumeMode: Filesystem
|
||||
@@ -2,21 +2,20 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jellyfin-redis
|
||||
name: redis
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jellyfin-redis
|
||||
app: redis
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jellyfin-redis
|
||||
app: redis
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
@@ -27,37 +26,40 @@ spec:
|
||||
- "20"
|
||||
- "1"
|
||||
ports:
|
||||
- name: redis
|
||||
containerPort: 6379
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
- ping
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
- ping
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- mountPath: /data
|
||||
name: data
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jellyfin-redis
|
||||
name: redis
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
type: ClusterIP
|
||||
internalTrafficPolicy: Cluster
|
||||
sessionAffinity: None
|
||||
selector:
|
||||
app: jellyfin-redis
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: redis
|
||||
protocol: TCP
|
||||
targetPort: redis
|
||||
selector:
|
||||
app: redis
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
||||
|
||||
@@ -5,13 +5,14 @@ metadata:
|
||||
name: jellyfin
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
type: ClusterIP
|
||||
internalTrafficPolicy: Cluster
|
||||
sessionAffinity: None
|
||||
selector:
|
||||
app: jellyfin
|
||||
ports:
|
||||
- name: http
|
||||
port: 8096
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
selector:
|
||||
app: jellyfin
|
||||
# Pin each client to one replica to reduce transcode-session churn/takeover.
|
||||
sessionAffinity: ClientIP
|
||||
type: ClusterIP
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
# HA: two replicas coordinate transcode session ownership through Redis and
|
||||
# resume each other's HLS segments off the shared RWX transcode PVC. Stable
|
||||
# pod names (jellyfin-0/1) are the lease owner identity, hence StatefulSet.
|
||||
replicas: 2
|
||||
serviceName: jellyfin
|
||||
podManagementPolicy: Parallel
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jellyfin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jellyfin
|
||||
spec:
|
||||
securityContext:
|
||||
# Group-write the shared RWX volumes and grant the render/video groups so
|
||||
# the runAsUser 1000 process can open the Intel DRI render node injected
|
||||
# by the device plugin.
|
||||
fsGroup: 1000
|
||||
supplementalGroups:
|
||||
- 44
|
||||
- 105
|
||||
- 109
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
affinity:
|
||||
# Spread the two replicas across nodes for node-level HA. Soft so a
|
||||
# single-GPU-node cluster still schedules both (i915 has 4 shared slots).
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: jellyfin
|
||||
topologyKey: kubernetes.io/hostname
|
||||
initContainers:
|
||||
# Select the fork's experimental PostgreSQL provider by writing
|
||||
# database.xml before Jellyfin starts. Runs as root to chown into the
|
||||
# shared config volume; mirrors the fork Helm chart's inject-db-config.
|
||||
- name: inject-db-config
|
||||
image: busybox:1.37.0
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
mkdir -p /config/config
|
||||
chown 1000:1000 /config/config
|
||||
chmod 775 /config/config
|
||||
cat > /config/config/database.xml << 'DBEOF'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<DatabaseConfigurationOptions>
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
<LockingBehavior>NoLock</LockingBehavior>
|
||||
</DatabaseConfigurationOptions>
|
||||
DBEOF
|
||||
chown 1000:1000 /config/config/database.xml
|
||||
chmod 664 /config/config/database.xml
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
containers:
|
||||
- name: jellyfin
|
||||
image: git.unkin.net/unkin/jellyfin-ha:v0.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8096
|
||||
protocol: TCP
|
||||
env:
|
||||
# Pod identity for the Redis transcode lease owner. The fork reads
|
||||
# JELLYFIN_INSTANCE_ID (falling back to MachineName); the stable
|
||||
# StatefulSet pod name gives each replica a unique lease identity so
|
||||
# takeover can target a dead replica. JELLYFIN_HA_POD_NAME is set for
|
||||
# parity with the fork Helm chart (nothing currently reads it).
|
||||
- name: JELLYFIN_INSTANCE_ID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: JELLYFIN_HA_POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
# Multiple replicas must not each answer UDP auto-discovery.
|
||||
- name: JELLYFIN_Network__AutoDiscovery
|
||||
value: "false"
|
||||
# Config dir must differ from the data root (Jellyfin sanity check).
|
||||
- name: JELLYFIN_CONFIG_DIR
|
||||
value: /config/config
|
||||
# Distributed transcode session store (jellyfin-ha additions).
|
||||
- name: Jellyfin__TranscodeStore__RedisConnectionString
|
||||
value: "redis:6379,abortConnect=false"
|
||||
- name: Jellyfin__TranscodeStore__LeaseDurationSeconds
|
||||
value: "30"
|
||||
# PostgreSQL main DB via the CNPG-generated app secret, routed through
|
||||
# the PgBouncer pooler. Composed with $(VAR) expansion so the password
|
||||
# is never rendered into the manifest; CNPG passwords are URL-safe.
|
||||
- name: PGUSER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: jellyfin-postgres-app
|
||||
key: username
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: jellyfin-postgres-app
|
||||
key: password
|
||||
- name: PGDB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: jellyfin-postgres-app
|
||||
key: dbname
|
||||
- name: POSTGRES_CONNECTION_STRING
|
||||
value: "postgresql://$(PGUSER):$(PGPASSWORD)@jellyfin-postgres-pooler:5432/$(PGDB)"
|
||||
- name: DATABASE_URL
|
||||
value: "postgresql://$(PGUSER):$(PGPASSWORD)@jellyfin-postgres-pooler:5432/$(PGDB)"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
gpu.intel.com/i915: "1"
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: 6Gi
|
||||
# Intel iGPU (QSV/VA-API) slot. Requesting it pins the pod to a
|
||||
# GPU-labelled node and injects /dev/dri/renderD* automatically, so
|
||||
# no /dev/dri hostPath or privileged container is needed. Enable
|
||||
# QSV/VA-API once in the Jellyfin admin UI; it persists to /config.
|
||||
gpu.intel.com/i915: "1"
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: transcode
|
||||
# Fork's real transcode temp path. RWX so a surviving pod reads the
|
||||
# in-flight .ts/.m3u8 segments of the pod it takes over. A per-pod
|
||||
# volume here silently breaks HA takeover.
|
||||
mountPath: /config/transcodes
|
||||
- name: cache
|
||||
mountPath: /cache
|
||||
- name: media
|
||||
mountPath: /media
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-config
|
||||
- name: transcode
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-transcode
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-media
|
||||
volumeClaimTemplates:
|
||||
# Per-pod scratch cache — RWO, disposable, one PVC per replica.
|
||||
- metadata:
|
||||
name: cache
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 30Gi
|
||||
storageClassName: cephrbd-fast-delete
|
||||
volumeMode: Filesystem
|
||||
@@ -4,6 +4,8 @@ metadata:
|
||||
annotations:
|
||||
configmap.reloader.stakater.com/auto: "true"
|
||||
secret.reloader.stakater.com/reload: "vault-ca-cert"
|
||||
# Replace clears the stale, API-server-defaulted spec.strategy.rollingUpdate that SSA cannot drop, which otherwise makes Recreate invalid.
|
||||
argocd.argoproj.io/sync-options: Replace=true
|
||||
labels:
|
||||
app.kubernetes.io/component: puppetserver
|
||||
app.kubernetes.io/instance: puppetserver
|
||||
|
||||
@@ -6,12 +6,11 @@ metadata:
|
||||
namespace: argocd
|
||||
spec:
|
||||
generators:
|
||||
- git:
|
||||
repoURL: https://git.unkin.net/unkin/argocd-apps
|
||||
revision: HEAD
|
||||
directories:
|
||||
# jellyfin only for now; downloads/managers apps join in later PRs.
|
||||
- path: apps/overlays/*/jellyfin
|
||||
- git:
|
||||
repoURL: https://git.unkin.net/unkin/argocd-apps
|
||||
revision: HEAD
|
||||
directories:
|
||||
- path: apps/overlays/*/jellyfin
|
||||
template:
|
||||
metadata:
|
||||
name: 'media-{{path[3]}}'
|
||||
@@ -23,10 +22,10 @@ spec:
|
||||
path: '{{path}}'
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: jellyfin
|
||||
namespace: '{{path[3]}}'
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- ServerSideApply=true
|
||||
- ServerSideApply=true
|
||||
|
||||
@@ -5,17 +5,15 @@ metadata:
|
||||
name: media
|
||||
namespace: argocd
|
||||
spec:
|
||||
description: Media services (jellyfin; downloads/managers namespaces to follow)
|
||||
description: Media services
|
||||
sourceRepos:
|
||||
- https://git.unkin.net/unkin/argocd-apps
|
||||
destinations:
|
||||
- namespace: jellyfin
|
||||
- namespace: 'jellyfin'
|
||||
server: https://kubernetes.default.svc
|
||||
clusterResourceWhitelist:
|
||||
- group: ''
|
||||
kind: Namespace
|
||||
- group: ''
|
||||
kind: PersistentVolume
|
||||
namespaceResourceWhitelist:
|
||||
- group: '*'
|
||||
kind: '*'
|
||||
|
||||
@@ -6,6 +6,16 @@ metadata:
|
||||
namespace: argocd
|
||||
data:
|
||||
kustomize.buildOptions: "--enable-helm"
|
||||
# Kubernetes defaults apiVersion/kind onto every StatefulSet
|
||||
# volumeClaimTemplates entry, but neither the raw manifests nor the helm
|
||||
# charts emit them, so live StatefulSets carry TypeMeta that git lacks.
|
||||
# volumeClaimTemplates are immutable on an existing StatefulSet, so ArgoCD
|
||||
# can never reconcile the removal and the resource stays perpetually
|
||||
# OutOfSync. Ignore the defaulted TypeMeta fleet-wide.
|
||||
resource.customizations.ignoreDifferences.apps_StatefulSet: |
|
||||
jqPathExpressions:
|
||||
- '.spec.volumeClaimTemplates[]?.apiVersion'
|
||||
- '.spec.volumeClaimTemplates[]?.kind'
|
||||
# External URL ArgoCD serves on (TLS terminated at the traefik-internal gateway).
|
||||
url: https://argocd.k8s.syd1.au.unkin.net
|
||||
# OIDC login via Authentik. The client secret is seeded in Vault out of band
|
||||
|
||||
Reference in New Issue
Block a user