--- # Forgejo pg_dump → restic → Hetzner Object Storage (DEV-514). # # Replaces the legacy volume-based backup that wrote pg_dump files to # an RWO PVC (`platform-backup-data`). The hcloud volume backing that # PVC was deleted externally on 2026-08-17; rather than re-provision # the same legacy pattern, this migrates to the restic→S3 pipeline # already used for loki/grafana/prometheus (DEV-485…DEV-492). # # initContainer runs pg_dump -F c into an emptyDir; main container # runs `restic backup --stdin-from-command` isn't used here because # the dump has to complete before restic starts (need a proper exit # code, and the custom-format dump is not resumable). Instead we # stage the dump on emptyDir and let restic dedup it into # `s3:${S3_ENDPOINT}/${S3_BUCKET}/restic/forgejo`. apiVersion: batch/v1 kind: CronJob metadata: name: forgejo-backup namespace: forgejo labels: app: forgejo-backup backend: restic spec: schedule: "0 3 * * *" concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: metadata: labels: app: forgejo-backup backend: restic spec: backoffLimit: 2 activeDeadlineSeconds: 3600 template: metadata: labels: app: forgejo-backup backend: restic spec: restartPolicy: OnFailure initContainers: - name: pgdump image: postgres:16-alpine imagePullPolicy: IfNotPresent command: - /bin/sh - -c - | set -eu echo "=== forgejo pg_dump started at $(date -u +%FT%TZ) ===" mkdir -p /dump PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump \ -h forgejo-postgres.forgejo.svc.cluster.local \ -U "${POSTGRES_USER}" \ -d "${POSTGRES_DB}" \ -F c \ -f /dump/forgejo-db.dump echo "pg_dump size: $(wc -c < /dump/forgejo-db.dump) bytes" # Manifest lets a restic-restore consumer identify the # dump without touching the binary. { echo "backup_timestamp=$(date -u +%Y%m%dT%H%M%SZ)" echo "type=postgresql_custom_dump" echo "database=${POSTGRES_DB}" echo "restore_cmd=PGPASSWORD= pg_restore -h -U ${POSTGRES_USER} -d ${POSTGRES_DB} -F c forgejo-db.dump" } > /dump/manifest.txt echo "=== forgejo pg_dump finished at $(date -u +%FT%TZ) ===" env: - name: POSTGRES_USER valueFrom: secretKeyRef: name: forgejo-postgres-secret key: POSTGRES_USER - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: forgejo-postgres-secret key: POSTGRES_PASSWORD - name: POSTGRES_DB valueFrom: secretKeyRef: name: forgejo-postgres-secret key: POSTGRES_DB volumeMounts: - name: dump mountPath: /dump resources: requests: cpu: 100m memory: 128Mi limits: cpu: 1000m memory: 512Mi containers: - name: restic image: harbor.basicstack.de/library/restic:0.19.1 env: - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: name: forgejo-s3-backup key: access-key - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: forgejo-s3-backup key: secret-key - name: RESTIC_PASSWORD valueFrom: secretKeyRef: name: forgejo-s3-backup key: restic-password - name: S3_ENDPOINT valueFrom: secretKeyRef: name: forgejo-s3-backup key: endpoint - name: S3_BUCKET valueFrom: secretKeyRef: name: forgejo-s3-backup key: bucket - name: RESTIC_REPOSITORY value: "s3:$(S3_ENDPOINT)/$(S3_BUCKET)/restic/forgejo" command: - /bin/sh - -c - | set -eu echo "=== backup-forgejo-restic started at $(date -u +%FT%TZ) ===" echo "Repository: ${RESTIC_REPOSITORY}" if restic snapshots >/dev/null 2>&1; then echo "Repo exists, skipping init." else echo "Repo missing, initialising..." restic init fi echo "--- restic backup /source ---" restic backup /source \ --tag forgejo \ --host k3s echo "--- restic forget/prune ---" restic forget --tag forgejo \ --keep-daily 7 \ --keep-weekly 4 \ --keep-monthly 6 \ --prune echo "--- restic check --read-data-subset=5% ---" CHECK_STATUS=0 restic check --read-data-subset=5% || CHECK_STATUS=$? echo "restic check exit: ${CHECK_STATUS}" echo "--- restic stats (repo size) ---" REPO_SIZE_BYTES=$(restic stats --json --mode raw-data 2>/dev/null \ | grep -oE '"total_size":[0-9]+' \ | head -1 \ | cut -d: -f2) REPO_SIZE_BYTES=${REPO_SIZE_BYTES:-0} echo "restic repo size: ${REPO_SIZE_BYTES} bytes" # Textfile-collector metrics, same wiring as # loki/grafana siblings (DEV-494). Atomic write. { echo "backup_forgejo_success $([ ${CHECK_STATUS} -eq 0 ] && echo 1 || echo 0)" echo "backup_forgejo_timestamp_seconds $(date +%s)" echo "backup_forgejo_check_status ${CHECK_STATUS}" echo "restic_repo_size_bytes{repo=\"forgejo\"} ${REPO_SIZE_BYTES}" } > /metrics/backup_forgejo.prom.tmp mv /metrics/backup_forgejo.prom.tmp /metrics/backup_forgejo.prom echo "=== backup-forgejo-restic finished at $(date -u +%FT%TZ) ===" exit ${CHECK_STATUS} volumeMounts: - name: dump mountPath: /source readOnly: true - name: metrics mountPath: /metrics - name: cache mountPath: /root/.cache/restic resources: requests: cpu: 100m memory: 128Mi limits: cpu: 1500m memory: 1Gi volumes: - name: dump emptyDir: sizeLimit: 5Gi - name: metrics hostPath: path: /var/lib/node_exporter/textfile_collector type: DirectoryOrCreate - name: cache emptyDir: {}