diff --git a/servers/legion/k8s/media/backup-cronjob.yaml b/servers/legion/k8s/media/backup-cronjob.yaml new file mode 100644 index 0000000..dce46b4 --- /dev/null +++ b/servers/legion/k8s/media/backup-cronjob.yaml @@ -0,0 +1,115 @@ +# Nightly media config backup — backs up all service config PVCs to Cloudflare R2 via restic +# NOTE: Does NOT back up actual media files (movies, TV, etc.) — those are re-downloadable. +# Only backs up app configs: Jellyfin, Radarr, Sonarr, Sonarr, Prowlarr, qBittorrent, Bazarr, Seerr. +# Schedule: 3am daily (offset from Gitea backup at 2am) +# Retains: 30 daily / 12 weekly / 6 monthly snapshots +apiVersion: batch/v1 +kind: CronJob +metadata: + name: media-backup + namespace: media +spec: + schedule: "0 3 * * *" + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 3 + successfulJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + spec: + restartPolicy: OnFailure + containers: + - name: backup + image: restic/restic:latest + command: + - /bin/sh + - -c + - | + set -e + + # Init repo if first run + restic snapshots 2>/dev/null || restic init + + echo "Backing up media service configs..." + restic backup \ + /jellyfin-config \ + /radarr-config \ + /sonarr-config \ + /prowlarr-config \ + /qbittorrent-config \ + /bazarr-config \ + /overseerr-config \ + --tag media \ + --tag legion \ + --host legion + + # Prune old snapshots + restic forget \ + --keep-daily 30 \ + --keep-weekly 12 \ + --keep-monthly 6 \ + --prune + + echo "Backup complete." + restic snapshots --latest 3 + envFrom: + - secretRef: + name: media-backup-credentials + volumeMounts: + - name: jellyfin-config + mountPath: /jellyfin-config + readOnly: true + - name: radarr-config + mountPath: /radarr-config + readOnly: true + - name: sonarr-config + mountPath: /sonarr-config + readOnly: true + - name: prowlarr-config + mountPath: /prowlarr-config + readOnly: true + - name: qbittorrent-config + mountPath: /qbittorrent-config + readOnly: true + - name: bazarr-config + mountPath: /bazarr-config + readOnly: true + - name: overseerr-config + mountPath: /overseerr-config + readOnly: true + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + volumes: + - name: jellyfin-config + persistentVolumeClaim: + claimName: jellyfin-config + readOnly: true + - name: radarr-config + persistentVolumeClaim: + claimName: radarr-config + readOnly: true + - name: sonarr-config + persistentVolumeClaim: + claimName: sonarr-config + readOnly: true + - name: prowlarr-config + persistentVolumeClaim: + claimName: prowlarr-config + readOnly: true + - name: qbittorrent-config + persistentVolumeClaim: + claimName: qbittorrent-config + readOnly: true + - name: bazarr-config + persistentVolumeClaim: + claimName: bazarr-config + readOnly: true + - name: overseerr-config + persistentVolumeClaim: + claimName: overseerr-config + readOnly: true diff --git a/servers/legion/k8s/media/backup-external-secrets.yaml b/servers/legion/k8s/media/backup-external-secrets.yaml new file mode 100644 index 0000000..e09d9b6 --- /dev/null +++ b/servers/legion/k8s/media/backup-external-secrets.yaml @@ -0,0 +1,29 @@ +# media-backup-credentials — R2 + restic credentials for nightly media config backup +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: media-backup-credentials + namespace: media +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: media-backup-credentials + creationPolicy: Owner + template: + data: + RESTIC_PASSWORD: "media-restic-2026" + AWS_ACCESS_KEY_ID: "{{ .r2_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ .r2_secret_access_key }}" + RESTIC_REPOSITORY: "s3:https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com/legion-media-backup" + data: + - secretKey: r2_access_key_id + remoteRef: + key: secret/data/r2 + property: access_key_id + - secretKey: r2_secret_access_key + remoteRef: + key: secret/data/r2 + property: secret_access_key