feat(k8s/jellyfin): add restic backup for database and configuration

This commit is contained in:
2026-09-13 16:14:04 +03:00
parent c34dc8790e
commit 8d99b0f7fe
3 changed files with 215 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
# 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.
# The volume is mounted read-write on purpose: jellyfin.db is in WAL mode, and
# SQLite cannot open a WAL database from a read-only filesystem once Jellyfin has
# closed it and removed the -wal/-shm files (e.g. while scaled to zero).
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
- |
set -e
apk add --no-cache sqlite
sqlite3 /db-local/jellyfin.db ".backup /backup/jellyfin.db"
volumeMounts:
- name: db
mountPath: /db-local
- 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.
# Intro Skipper keeps its results in two live WAL-mode SQLite databases, so they
# are snapshotted with sqlite3 ".backup" rather than copied as files (which could
# capture a torn page mid-write). Read-write mount for the same reason as above.
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
initContainers:
- name: sqlite-backup
image: alpine:3.21
command:
- sh
- -c
- |
set -e
apk add --no-cache sqlite
mkdir -p /backup/introskipper
for db in introskipper.db introskipper-cache.db; do
sqlite3 "/config/data/introskipper/$db" ".backup /backup/introskipper/$db"
done
volumeMounts:
- name: config
mountPath: /config
- name: backup-tmp
mountPath: /backup
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
- name: backup-tmp
mountPath: /backup
volumes:
- name: secrets
secret:
secretName: jellyfin-backup-config
- name: config
persistentVolumeClaim:
claimName: jellyfin-config
- name: backup-tmp
emptyDir: {}
@@ -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:A1EdRDhXncj5EIjg6iyw+mmiW7ObrGVlBmnQn1GJbF6P+yHmw97h4Hk/yHfMM1tDrNhiy8jpMWmXHGuHa56QFGPQzPCkaMtjqwO2pUcXrdrv8ThVtX3ZgyzvHX1fL5lwDm5aG0mMjuV51+j5URR6P3n+GCCg3OYP/DbEngroT5f7QKhFb4hwHdGnF9tHLwKH/hZrXJIH8XLeJD5BbjHBRFiXEpYlgmHtZ3Vb3SzJRMSMBMH4wBj2obxL2nOqKDXaK/NRV1XHuN+/enKvQaFm9j3h7F4G4MD1Gfr6LwjtGiV5S8ngcQo9gQptardTVqxTtKHKA80pbtSoqwP3TuR6Vh6U9KNqDiaLte6wqoOxQ6Tb3oCTnFJOLn1WE/PBxTPp9VDRfwXBlhPtYK8JhaTcQHdjkM1xw3qzyL09XMNKWf1wvlaoszurwlxI+bSKNlDQjC/dUd/j067xFf1SRNze++75QJkrp1o2+4DCrdK4QiPd9FeacgUml/uI957i0ZkBQLTn9WHUcp7kLmALzu8byDHgf2jkHYEfJ3QKg5yjpE0aQ2Iub/8fnQC732YOUGeP5HKcFTl8fU4xdRrIPbz8K728FYukeU4pzLQY+2qOENfMSFoVMIYpD5LSyv+RqIejb6x1ZYIchPQ91aBsnkXRSL6b2Lj60vccF1cyilrPAgBAzM6FeAvAHqbalSMdvyMwd6AgHgW0jOE9dGquxQfliCyoU9vcsNKY/2y01MWiH7wa80IjvrrUTiuQHh7h2g+JYqiXoyA2R9qXQBScjiWQbtUAYQjBqqKGdTSxiUzYiCBw75FMJxj2BUE739bQQNCBN18WLSyietS7LiTu50cL0jD79zNYGqi4vduHdK+MZuhZa5I/5OoHo71VGj1nkyKYBOemA5zdW3xbS4ZOnblOYeEkhx0It1KafAbN4JnY7ZPCQqH7JyAxKIV1YR4zKEMpFgEzIKUFl0Cr7PrZ5/hoA6HhuzzWq6GvtQ+4PJusp5Spu+d7AEZLSPqPrSlK6iCQA3nKG+A/wau4BI4P83Q2LzW5Aa4aujNZJ+mCYAyPkkQqt6+MTWgFCKa61fZT8Rypw+q1+BRJrY+/2PU/DUeEBb6hBTcL24YRHEkcPlcU4FkK4/WppALM/FWySFgfdwL9XqhOWwuhJ3nfMFEMFR9TkbrTHGNzC7JRTcIo4FIhvgoNtQ2WujgB01a79cqGcw2sSWDTeUCH5SaeYiQSfmI1n7OyBP9vk3pc8FrqRVZkpetlrYCdart6FKDuO+P4YhOpk42GrL+0lWis4Hkmos9REpKP0jAkvgcI9nDHTq1stHbPuqVXsBaah0mrbk450ZAREmGMx/4Gc3ltXh8YfgT26HSDCl3SRtXgnLsTA9XKYgR1zgvT8a82EYDg08af5X1yGQFutxIkfIwIijAIx+Y9NY6KeWvgBYxjgkKkNvY29kCZZ5POKw5L9YiMOcp4jSTK+dNdPqCWplSzyaLgrFGBbzOcQVagm9UkZEXLv44H60fMMExdHzuo81Bh1JV6luVqqDcHlnigxM6QNMNavgT6G60kFfRmEyRldSrpefKkH9GtxJMJgWMgZhhdDkkOzFsou/GuaWCLQp3JyOmZiKisJu5Nytkl4h0rPGR+Oe26Z5IxCYOjRgmp8+TT4JMmHIvlL7Ljf1PRQS6eWz9ICvoK/nvLaj0/rjEvQ9BZKY1WVzGGSYrvrNzNbprhaT6+DE1Xdd9qmwaP+B9czB54KCrKd17knL0GWYkneOM/M+UNeD4nDeK8fShHJeIYI5+7QRh54BsGlmwgw8mYnysagsQeav56aZC7Lcg3e1uCeVSHY7RdrUJ7i+++5v95TpIP8tQCFO5RvlIp7UPChdHOyxqlGqGMWs66Jvpq47ENN6OObAdR0uAHPA/MsvVO0ygmANE8Sog2JLJbQO6+3Wsmg/n+Is2Ps1lEjSft6Kn/SrmPG3ilOedpirrnn1cfWOHAXtfruZ9xB2GIdhBHgVFgjSUmAvd+9R+MV+urq3d/Czr1+w2WDryOPPw=,iv:+DuENxnJKTQ3ZKom2uNsW7QU1eMsrO+ZFCo5aW51pwE=,tag:4MiKklkRuvJuCvlu8tV07w==,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-13T13:09:04Z"
mac: ENC[AES256_GCM,data:E8xZZ+0hdRPKE/nss00gopEg7feF7t4pXBqYkEtr9yzHn1Vz4Opfv4vBDDgwqntZdekE40rpzWTuPJDAfOrhDsCsgZjOgWa6VpSRNM9GQ37Ax/fv/G42CXtwBPCzY1p2GuXr9nsJI+/P4c6sx4FIXZ0o4QLCHuStkYpjbHlyIFA=,iv:LJCUP/KbZ0tZPdG2hedHfZMfYWS+w4T73JaI7nLe2uQ=,tag:J7GT2jm6iHFZ8oMV2ecEIw==,type:str]
version: 3.13.3