feat(k8s/jellyfin): add restic backup for database and configuration
This commit is contained in:
@@ -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
|
||||||
@@ -45,3 +45,29 @@ spec:
|
|||||||
protocol: UDP
|
protocol: UDP
|
||||||
- port: 1900
|
- port: 1900
|
||||||
protocol: UDP
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user