blumeops/docs/how-to/mealie/restore-from-borg.md
Erich Blume 72b27b7fd2 C0: docs — add mealie borg restore how-to
Captures the procedure used to restore mealie's SQLite DB from a borgmatic
archive after the post-DR wipe: extract from borg, snapshot the wiped DB,
swap via a helper pod on the ReadWriteOnce PVC, fix UID 911 ownership.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:04:28 -07:00

5.7 KiB

title modified last-reviewed tags
Restore Mealie from Borg 2026-04-24 2026-04-24
how-to
mealie
backup

Restore Mealie from Borg

How to restore mealie's SQLite database from a borgmatic archive when data has been lost (e.g. PVC wiped, accidental deletion, post-DR rebuild).

Prerequisites

  • SSH access to indri (where borgmatic runs and stores k8s SQLite dumps)
  • Mealie deployment present in the cluster (the PVC mealie-data exists in namespace mealie)
  • Know which borg archive predates the data loss

Procedure

1. Identify a Pre-Loss Archive

List archives and pick one before the incident:

ssh indri 'BORG_PASSCOMMAND="cat /Users/erichblume/.borg/config.yaml" \
  /opt/homebrew/bin/borg list /Volumes/backups/borg | tail -30'

Compare dump sizes across archives if you're unsure when the loss happened — the daily borgmatic run captures /Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db. A sudden drop in size signals the wipe:

ssh indri 'bash -c "BORG_PASSCOMMAND=\"cat /Users/erichblume/.borg/config.yaml\" \
  /opt/homebrew/bin/borg list /Volumes/backups/borg::<archive-name> \
  --pattern=+Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db"'

2. Extract the Pre-Loss Dump

ssh indri 'mkdir -p ~/tmp/mealie-restore && cd ~/tmp/mealie-restore && \
  BORG_PASSCOMMAND="cat /Users/erichblume/.borg/config.yaml" \
  /opt/homebrew/bin/borg extract /Volumes/backups/borg::<archive-name> \
  Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db'

The file lands at ~/tmp/mealie-restore/Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db (borg preserves the full path).

3. Verify the Extracted DB

ssh indri 'sqlite3 ~/tmp/mealie-restore/Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db \
  "PRAGMA integrity_check; SELECT COUNT(*) FROM recipes; SELECT COUNT(*) FROM users;"'

Expect ok and non-zero recipe/user counts.

4. Snapshot the Current (Wiped) DB

Belt and suspenders — keep a copy of the live DB before overwriting, in case the restore goes wrong:

ssh indri 'bash -c "kubectl --context=minikube -n mealie exec deploy/mealie -- \
  python3 -c \"import sqlite3; sqlite3.connect(\\\"/app/data/mealie.db\\\").backup(sqlite3.connect(\\\"/tmp/wiped-mealie.db\\\"))\" && \
  POD=\$(kubectl --context=minikube -n mealie get pod -l app=mealie -o jsonpath=\"{.items[0].metadata.name}\") && \
  kubectl --context=minikube cp mealie/\$POD:/tmp/wiped-mealie.db /Users/erichblume/tmp/mealie-restore/wiped-mealie.db"'

5. Scale Mealie Down

The PVC is ReadWriteOnce, so the helper pod can't mount it while mealie is running:

ssh indri 'kubectl --context=minikube -n mealie scale deploy/mealie --replicas=0 && \
  kubectl --context=minikube -n mealie wait --for=delete pod -l app=mealie --timeout=60s'

6. Start a Helper Pod on the PVC

ssh indri 'bash -c "cat > /tmp/mealie-helper.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: mealie-restore
  namespace: mealie
spec:
  restartPolicy: Never
  containers:
    - name: helper
      image: alpine:3.20
      command: [\"sh\", \"-c\", \"sleep 3600\"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: mealie-data
EOF
kubectl --context=minikube apply -f /tmp/mealie-helper.yaml && \
  kubectl --context=minikube -n mealie wait --for=condition=Ready pod/mealie-restore --timeout=60s"'

7. Copy and Swap the DB

ssh indri 'bash -c "kubectl --context=minikube cp \
  /Users/erichblume/tmp/mealie-restore/Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db \
  mealie/mealie-restore:/data/mealie.db.restored && \
  kubectl --context=minikube -n mealie exec mealie-restore -- sh -c \
  \"mv /data/mealie.db /data/mealie.db.wiped && mv /data/mealie.db.restored /data/mealie.db && chown 911:911 /data/mealie.db\""'

Mealie's container runs as UID 911. kubectl cp preserves the host UID (501 on indri), so the chown is required — without it, mealie may fail to write to the DB.

8. Tear Down the Helper and Scale Back Up

ssh indri 'kubectl --context=minikube delete pod -n mealie mealie-restore --wait=true && \
  kubectl --context=minikube -n mealie scale deploy/mealie --replicas=1 && \
  kubectl --context=minikube -n mealie wait --for=condition=Available deploy/mealie --timeout=120s'

9. Verify the Restore is Live

ssh indri 'bash -c "kubectl --context=minikube -n mealie exec deploy/mealie -- \
  python3 -c \"import sqlite3; c=sqlite3.connect(\\\"/app/data/mealie.db\\\"); \
  print(\\\"recipes:\\\", c.execute(\\\"select count(*) from recipes\\\").fetchone()[0]); \
  print(\\\"users:\\\", c.execute(\\\"select count(*) from users\\\").fetchone()[0])\""'

The counts should match what you saw in step 3.

10. Clean Up

Once mealie is working and you've confirmed the data, remove the in-PVC safety copy:

ssh indri 'kubectl --context=minikube -n mealie exec deploy/mealie -- rm -f /app/data/mealie.db.wiped'

Leave the host-side copy at ~/tmp/mealie-restore/wiped-mealie.db until the next borgmatic run captures the restored state, in case you need to roll back.

Notes

  • The active kubectl context for mealie is minikube on indri until the ringtail migration completes. Update the --context flag if mealie has moved.
  • OIDC sign-ins after the wipe may have created new user rows; the restored DB will replace them. Affected users will sign in fresh and Authentik will re-link them on next login.