diff --git a/kubernetes/app/jellyfin/cronjob-backup.yaml b/kubernetes/app/jellyfin/cronjob-backup.yaml new file mode 100644 index 0000000..33335fb --- /dev/null +++ b/kubernetes/app/jellyfin/cronjob-backup.yaml @@ -0,0 +1,135 @@ +# Jellyfin SQLite database backup. +# jellyfin.db lives on the local-path PVC (/config/data/jellyfin.db is a symlink +# to it), so it is read from jellyfin-data directly. sqlite3 ".backup" uses the +# online backup API — no downtime, consistent snapshot while Jellyfin runs. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: jellyfin-db-backup + namespace: jellyfin + labels: + app: jellyfin-backup +spec: + schedule: "30 2 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + metadata: + labels: + app: jellyfin-backup + spec: + restartPolicy: OnFailure + initContainers: + - name: sqlite-backup + image: alpine:3.21 + command: + - sh + - -c + - | + apk add --no-cache sqlite + sqlite3 /db-local/jellyfin.db ".backup /backup/jellyfin.db" + volumeMounts: + - name: db + mountPath: /db-local + readOnly: true + - name: backup-tmp + mountPath: /backup + containers: + - name: resticprofile + image: creativeprojects/resticprofile:0.32.0 + command: + - sh + - -c + - | + resticprofile -c /secrets/profiles.yaml -n jellyfin-db backup + resticprofile -c /secrets/profiles.yaml -n jellyfin-db copy + env: + - name: B2_ACCOUNT_ID + valueFrom: + secretKeyRef: + name: jellyfin-backup-config + key: B2_ACCOUNT_ID + - name: B2_ACCOUNT_KEY + valueFrom: + secretKeyRef: + name: jellyfin-backup-config + key: B2_ACCOUNT_KEY + volumeMounts: + - name: secrets + mountPath: /secrets + readOnly: true + - name: backup-tmp + mountPath: /backup + volumes: + - name: secrets + secret: + secretName: jellyfin-backup-config + - name: db + persistentVolumeClaim: + claimName: jellyfin-data + - name: backup-tmp + emptyDir: {} +--- +# Jellyfin configuration backup. +# Only non-regenerable state: server config, library definitions, plugin +# configuration (including the Authelia SSO settings), collections, and the +# Intro Skipper analysis results. Metadata (27G), trickplay (20G) and extracted +# subtitles (1.7G) are deliberately excluded — they are regenerable from the +# media files and the metadata providers. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: jellyfin-config-backup + namespace: jellyfin + labels: + app: jellyfin-backup +spec: + schedule: "30 3 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + metadata: + labels: + app: jellyfin-backup + spec: + restartPolicy: OnFailure + containers: + - name: resticprofile + image: creativeprojects/resticprofile:0.32.0 + command: + - sh + - -c + - | + resticprofile -c /secrets/profiles.yaml -n jellyfin-config backup + resticprofile -c /secrets/profiles.yaml -n jellyfin-config copy + env: + - name: B2_ACCOUNT_ID + valueFrom: + secretKeyRef: + name: jellyfin-backup-config + key: B2_ACCOUNT_ID + - name: B2_ACCOUNT_KEY + valueFrom: + secretKeyRef: + name: jellyfin-backup-config + key: B2_ACCOUNT_KEY + volumeMounts: + - name: secrets + mountPath: /secrets + readOnly: true + - name: config + mountPath: /config + readOnly: true + volumes: + - name: secrets + secret: + secretName: jellyfin-backup-config + - name: config + persistentVolumeClaim: + claimName: jellyfin-config diff --git a/kubernetes/app/jellyfin/networkpolicy.yaml b/kubernetes/app/jellyfin/networkpolicy.yaml index 7545038..4147fcc 100644 --- a/kubernetes/app/jellyfin/networkpolicy.yaml +++ b/kubernetes/app/jellyfin/networkpolicy.yaml @@ -45,3 +45,29 @@ spec: protocol: UDP - port: 1900 protocol: UDP +--- +# Backup jobs need DNS, the Synology rest-server (8888) and B2 over HTTPS. +# Declaring egress here also confines the backup pods to exactly those. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-backup-egress + namespace: jellyfin +spec: + podSelector: + matchLabels: + app: jellyfin-backup + policyTypes: + - Egress + egress: + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - ports: + - port: 8888 + protocol: TCP + - ports: + - port: 443 + protocol: TCP diff --git a/kubernetes/app/jellyfin/secret-backup.sops.yaml b/kubernetes/app/jellyfin/secret-backup.sops.yaml new file mode 100644 index 0000000..a90fa4c --- /dev/null +++ b/kubernetes/app/jellyfin/secret-backup.sops.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: jellyfin-backup-config + namespace: jellyfin +stringData: + profiles.yaml: ENC[AES256_GCM,data:79dX812uHpYe8pHXAMEvv7Ig+d7cmCM1LtQ7OzofEDd9ReCYQEonT5mry4JWE6a7P5xIS09ckuHt3R5oqj8KO/KQwtlagyxuERQEWDUVMr1lXuM0H3V4O7HwsY/q1XgA16BZDhsi8yi5QIXNwDuew1dD1quasB5OPS8JRGZgeTP5Kj4MNtfEiese7pILfmtPoLxpMVC0kc9M0A2zzLLQba51t2vH5PQD7TlNmrLYsbGPBP2btr58yruKlVhWY/bBEnpI4WFkcpHhmFr3xlE0KAaxton4SC38Vdo3aL/Z79wOH0f50uFXLSRf/7LkRxJok61Hc/27QEVjrg78y3UYGApIz+16S9UAlZT33pDgSqKvlWj+jxzw0nclc8K+KWRHDUArQz5n0RaxtKRs5/mel2zgL1+eZ6wG1472b3Ct059GBRS5VsIz4NVc6spI582A48PfkJWy/auMmdLKdUCDgZ8El23Tl+d9vdS6mWA/+ijrYeDX0ZkOogzvhFw4xlA7M+JS3MJCCiE3LdjMYmoGOW835m6+u0tEDvmLm3IEfFfOiMdRR5VsoupOHwWHXTOtM3R2wXEql+rAWflc+P+tU5Qp6x7NOf8NdDZIvSkXPR/Zn4LBjrJkXq/whd5uPExBotzuUx+JedMUiuI1WvI7BxVSMMP1R8IN3J1/oEMpyyi6MekxMyNgRRdWjo3E8SG+RG4sXkDEu7DFsuGwR1skqomuE0l9IpTy7Xgif8APIKtc8B4uGJakKvNVTkSpAdx1Op2uPdH5EBCF0hc7PnpwWse6l2+YS4G/ynczYGPx/xbYVX8IpPty/Mg3a4On+CvBmAsmj90LYloAuqNiGiTp7+Np9n94IWpYCgO7T5FtU0/2w0O0tgULPE8BLKZOhWfKS5pIuq8BUbbzG2O41855OqAjMUhwzbDLJp3ES77DnuQ68tYvpAo2cAXpK7YdEUzB5Nwyev5Yh8Ljtz68mAk1W1xxKh92C6CnL+RaOKZM4AAXUnKfhn/D1iFnPGtXPbI2UZ+oQd/Uk45jD7kuyw2eRT9Fntk+zV49X6kPx5bqfMKISn7AKuxlMumQ91k0MP/GsF+FbZr74HmKaOIQgv4sd65qDL14eSpdw/17l1q6VuCG+k2pWdyUxRx8MVZM2OWdPfd62U/dwY/blFi1x7XIRhyAwg7HBFuhRsUilLLzR0GGG6Jmw5PcNC9OTX+ybsSkyvV/lAWC29/FbqoSXOpMVngfF7NsHg1MuaxF19g5JjlTNCiXjWZG01v/MgyVtxwo8ldwGpnRLuFYHD/sv1dFsVXRMMYvqCHM6aCVzT4zcfUCTAVhMfc2yVRdPjN3qjcVdwMyL+OvQFEH6+d1yzz2G8ua8U0tp2YU3aCyCl47ZnqbPBeICluG9PXDGLCoAsTKVIY2VTHiX8XItoWj4AnyGxseWikPCyPIPrG8stALfNr9wqewd//y6Bqpw+OS5XWzpNKtRVlKToTlIDCvwA9HpLHqMsF9xSQzYuua7xxoeEMAHH1bZdgA/BR0QWNOhyXr0qeA2c2hjgIX7eUKVVyDV4enLcHeOQXf1PWxvqU/fD2fm7jiy5g4tD3sAj+S0B/0WyRrpO9uE2ASudRkcG6tsu9kz7gF/hDQ8qns4+yKQJtSiL32wk+QaOREDxRL8LFRTCsxJKyXWlW8J5SR0Q94fHWGVg+Fh0Z71TonBdYvOvHbNYz868jpc4pOOnF+062qj2sN5fL92ao1mCBNJR+oTcVP3QA1HV0cNpLY/dWwXV/vqePZsOzWv8T6F+6UvYBzxemNT2H8BhXICW8pNIHHDD2I1sH/fVVb2d/UqX+fWfkVbUEfey++LSuPMvPT37kuQMgDsa83sbT8QQjq,iv:eNJPPE2ERcZjTgFi8JvzUvHIiz88G/HR2ifpVpL6XOs=,tag:M9YehKaAk4H6wWZsF1Bb3g==,type:str] + restic-password-nas: ENC[AES256_GCM,data:2uDszBmHa4YG1oYyhN4Wjw0Lcc6/MpmrCHldwNF22kFiokbx4R+zrd8ML4NvK1ezmQmMDhlb3RsyAcQNRgJ9Ng==,iv:96pdW1B6M97+eG1EC2YPbNmO4OQmt47uiJOWuvzdYzE=,tag:dQKHD+2LeWL5+tcQbrbN0g==,type:str] + restic-password-b2: ENC[AES256_GCM,data:ceHYbKn174cIm38qnlGvx9edxP8apPWkwx7wAkNncSF6kIE5OvXVsNAWcjsZKQLEgYXFX5NwLZMDPY0edtAhNQ==,iv:mDki/Soljcnp60cfihK2EqfTNHv/VnoieetBThFqLtE=,tag:5VMRM0fe4nFDal3BO+ninw==,type:str] + B2_ACCOUNT_ID: ENC[AES256_GCM,data:6u+UlYRILbb0TRTdISi7TdVItw4omfodFw==,iv:A+be6o3ZjfYsyvEk750DcoSlBsKYM2FncSY8kqkcpqs=,tag:9ReTmil5sAfo+04IfdaKYw==,type:str] + B2_ACCOUNT_KEY: ENC[AES256_GCM,data:p+irC0iQR3RjhmElD+tZcXcgWsV8ROGHsEOJ7kxXvg==,iv:2VgptxIaJ0v1e5jejLyBIPHnvxD6ldgTWn/3oa1M32Y=,tag:9gHDOD0524IAoQQLOytT+w==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5Mk5NMVNxOG5JWHhkMjlh + YzRyYVhYT2tncmVzRzB5NXc2aEZQcUVtVFUwCnl2U2JQSkdpTjZVb1pPTmlPeVRt + SURJNFFzUTFTdVZpckV5d2puN2xQTmcKLS0tIDUxeXFLdGxOdDVLdng2SFRWb296 + NFpDSkZmYkY0Vyt4Nml3N3VXbFdZZ0kKfNoqtyb+16qpDdCHgp3uiDCHqtiDXEjB + p1kaQkgF6xqFohomV+6x0RsW+r7PaGh8oWRut0NBRahZwIKBucZUTw== + -----END AGE ENCRYPTED FILE----- + recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc + encrypted_regex: ^(data|stringData|email)$ + lastmodified: "2026-09-13T12:57:35Z" + mac: ENC[AES256_GCM,data:zCzFD9/CmXjME/I8i+91Xcg8iHkQXstksi4y7TG87E75UUCAeHr0q62yNThJIhwLMS7RXr62eZH0Gg0l13De2yDrZdI9FDr+65PiniSTsvTQSSCXt8BWupn/cAZJkbxhUb7lBxzfr2IV2voV1TH2uCZagvKNymMl15g+uQBrMro=,iv:vkkA3jjJ2o7eNrTIDlY9CPqMs4byet3LQHdPvmHOfOE=,tag:4//bTvkj9BlGSi6McD+IGw==,type:str] + version: 3.13.3