--- # Minecraft world backup — hourly CronJob, 3-day local retention # # Strategy (two-tier): # Short-term : this CronJob — hourly tarballs on papermc-backups PVC, 72-hour retention # Long-term : Pure FlashArray protection group snapshots on utilidor (managed separately) # # Backup sequence: # 1. RCON save-all — flush all dirty chunks to disk # 2. RCON save-off — pause auto-save to keep the world consistent during tar # 3. tar world directories to /backups/world-YYYY-MM-DDTHH-MM.tar.gz # 4. RCON save-on — re-enable auto-save # 5. Prune backups older than 3 days # # RCON password sourced from the papermc-rcon Secret (ESO-managed, same as server). # The backup pod mounts both PVCs read-write; world-data is safe because the # server has already quiesced saves via RCON before the tar runs. # # Note: both PVCs are RWO. The backup job runs only while the main server pod is # running (RCON is reachable), so there is no volume attach conflict — they are # mounted on the same node by the scheduler. If the server pod is down, the backup # job will fail at the RCON step, which is correct behavior (nothing to back up). apiVersion: batch/v1 kind: CronJob metadata: name: minecraft-backup namespace: minecraft labels: app.kubernetes.io/name: minecraft app.kubernetes.io/component: backup spec: schedule: "0 * * * *" # every hour on the hour concurrencyPolicy: Forbid # skip if a previous backup is still running successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: backoffLimit: 0 # don't retry — a partial backup is worse than no backup template: metadata: labels: app.kubernetes.io/name: minecraft app.kubernetes.io/component: backup spec: restartPolicy: Never securityContext: runAsNonRoot: false fsGroup: 1000 containers: - name: backup # Same image as the server — has both /bin/sh and rcon-cli built in. # itzg/rcon-cli is distroless (no shell); don't use it for scripted jobs. image: itzg/minecraft-server:2026.7.0 imagePullPolicy: IfNotPresent env: - name: RCON_HOST value: "journey-into-imagination.minecraft.svc.cluster.local" - name: RCON_PORT value: "25575" - name: RCON_PASSWORD valueFrom: secretKeyRef: name: papermc-rcon key: rcon-password command: - /bin/sh - -c - | set -e TIMESTAMP=$(date -u +%Y-%m-%dT%H-%M) BACKUP_FILE="/backups/world-${TIMESTAMP}.tar.gz" RETAIN_DAYS=3 echo "[backup] Starting backup at ${TIMESTAMP}" # Step 1: quiesce the world echo "[backup] Flushing chunks (save-all)..." rcon-cli save-all echo "[backup] Pausing auto-save (save-off)..." rcon-cli save-off # Step 2: archive world directories (nether/end may not exist yet) echo "[backup] Archiving world to ${BACKUP_FILE}..." DIRS_TO_BACKUP="world" [ -d /data/world_nether ] && DIRS_TO_BACKUP="$DIRS_TO_BACKUP world_nether" [ -d /data/world_the_end ] && DIRS_TO_BACKUP="$DIRS_TO_BACKUP world_the_end" FILES_TO_BACKUP="" [ -f /data/whitelist.json ] && FILES_TO_BACKUP="$FILES_TO_BACKUP whitelist.json" [ -f /data/ops.json ] && FILES_TO_BACKUP="$FILES_TO_BACKUP ops.json" [ -f /data/banned-players.json ] && FILES_TO_BACKUP="$FILES_TO_BACKUP banned-players.json" [ -f /data/banned-ips.json ] && FILES_TO_BACKUP="$FILES_TO_BACKUP banned-ips.json" tar -czf "${BACKUP_FILE}" -C /data $DIRS_TO_BACKUP $FILES_TO_BACKUP || { echo "[backup] ERROR: tar failed — re-enabling saves and exiting" rcon-cli save-on exit 1 } echo "[backup] Archive complete: $(du -sh ${BACKUP_FILE} | cut -f1)" # Step 3: resume auto-save echo "[backup] Resuming auto-save (save-on)..." rcon-cli save-on # Step 4: prune old backups echo "[backup] Pruning backups older than ${RETAIN_DAYS} days..." find /backups -name "world-*.tar.gz" -mtime +${RETAIN_DAYS} -delete REMAINING=$(find /backups -name "world-*.tar.gz" | wc -l) echo "[backup] Done. ${REMAINING} backup(s) retained." volumeMounts: - name: world-data mountPath: /data readOnly: true - name: backups mountPath: /backups volumes: - name: world-data persistentVolumeClaim: claimName: papermc-world-data - name: backups persistentVolumeClaim: claimName: papermc-backups