Files
kvitt/infra/RESTORE.md
T
jacobh2 6e5981d62a Pi: minimal self-contained compose project (D-125)
The Kvitt Pi runs Kvitt from /home/jacobhagstedt/kvitt: compose.yaml, .env,
secrets/ and relative bind mounts ./data, ./backups, ./import. Both containers
run as the owner's uid/gid (KVITT_UID, KVITT_GID). No install script, no /opt,
no Docker daemon or system changes; operations are plain docker compose
commands (README "Pi: what the owner does", RESTORE.md).

Removed install.sh, deploy.sh, bootstrap.sh, backup-now.sh, install-apk.sh,
lib.sh. .env holds only the keys compose.yaml reads; RCLONE_REMOTE is fixed
to gdrive: in compose.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-20 19:18:53 +02:00

3.2 KiB

Restore Kvitt from a Drive backup (SPEC §10.9)

Run on the Kvitt Pi, in ~/kvitt (/home/jacobhagstedt/kvitt), as jacobhagstedt. The files in data/ and backups/ are yours (both containers run as your uid, D-125), so they are moved with plain mv and cp; only rclone and sqlite3 come from the running backup container. Nothing is deleted: the current database and pictures are moved aside. Do not run it around 03:30, when the nightly backup runs.

cd ~/kvitt
FOLDER=$(sed -n 's/^RCLONE_FOLDER_ID=//p' .env)

1. Stop the server

docker compose stop server

2. Download the chosen backup from Drive to the Pi

List what is on Drive (newest last) and pick a stamp:

docker compose exec -T backup sh -c 'c=$(mktemp); cp /config/rclone/rclone.conf "$c"; RCLONE_CONFIG=$c rclone lsf gdrive: --drive-root-folder-id "$1" --files-only; rm -f "$c"' sh "$FOLDER" | sort
STAMP=20261102T013000Z            # the stamp of the pair you chose

Download both files into backups/restore/:

mkdir -p backups/restore
docker compose exec -T backup sh -c 'set -e; c=$(mktemp); cp /config/rclone/rclone.conf "$c"
    for f in "kvitt-$2.sqlite.gz" "pictures-$2.tar.gz"; do
      RCLONE_CONFIG=$c rclone copy "gdrive:$f" /backups/restore/ --drive-root-folder-id "$1"
    done; rm -f "$c"' sh "$FOLDER" "$STAMP"
ls -l backups/restore

(A file downloaded in the browser instead: copy it from the Mac with scp <file> jacobhagstedt@<kvitt-pi-address>:kvitt/backups/restore/.)

3. Unpack and check

gunzip -f "backups/restore/kvitt-$STAMP.sqlite.gz"
docker compose exec -T backup sqlite3 "/backups/restore/kvitt-$STAMP.sqlite" "PRAGMA integrity_check"

It must print ok. Anything else: stop here and pick another backup.

4. Move the current data aside and put the backup in place

aside="data/pre-restore-$(date -u +%Y%m%dT%H%M%SZ)"; mkdir "$aside"
for f in data/kvitt.sqlite data/kvitt.sqlite-wal data/kvitt.sqlite-shm data/pictures; do
  [ -e "$f" ] && mv "$f" "$aside/"
done
cp "backups/restore/kvitt-$STAMP.sqlite" data/kvitt.sqlite
tar -xzf "backups/restore/pictures-$STAMP.tar.gz" -C data
echo "previous data kept in $aside"; ls -l data

5. New sync epoch

docker compose run --rm server rotate-epoch

It prints the new epoch. The phones see it and re-push their data (§14.4.8).

6. Start the server and check health

docker compose start server
docker compose ps server          # healthy within a minute
curl -fsS http://localhost:8080/health

The last line prints {"status":"ok",...} (the server's own port; the path through Nginx Proxy Manager is unchanged by a restore).

7. The phones

Open Kvitt on both phones (owner's and partner's) and leave it open for a minute; the next sync detects the new epoch and re-pushes the phone's data (if nothing seems to happen, close the app and open it again). Check that both phones show the same balances on Översikt (yes/no).

Afterwards

When the phones agree, the downloaded files can go: rm -r backups/restore. The data/pre-restore-<stamp>/ folder stays until you decide otherwise. Run docker compose exec backup /backup.sh once to put the restored state on Drive.