The owner copies infra/pi/ to the Kvitt Pi and runs it there (D-100); nothing
on the Mac connects to the Pis. compose stack of SPEC §10.2 with images
${KVITT_REGISTRY}/kvitt-{server,cert-sync,backup}, Caddyfile §10.5.2 verbatim,
.env.example with the OQ-3 values as CHANGE_ME placeholders, install.sh,
deploy.sh (backup before every deploy), bootstrap.sh, backup-now.sh,
install-apk.sh. cert-sync image and sync.sh (§10.5.1, tar stream for both
authorized_keys variants); backup.sh uploads with rclone copy --immutable and
deletes local files once both are confirmed on Drive (D-002); both sidecars
schedule with supercronic because busybox crond runs no jobs as non-root;
backup runs as uid 10001. .gitea/workflows/build-images.yaml from the owner's
example (not yet run); infra/build-images.sh and infra/publish-apk.sh (R8
mapping kept, version per §10.6) on the Mac; README checklist and RESTORE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
3.2 KiB
Restore Kvitt from a Drive backup (SPEC §10.9)
Run on the Kvitt Pi, in /opt/kvitt, as the owner's user. Every command uses the running backup container,
which has sqlite3, rclone, the Drive credentials and the data and backups volumes, and runs as uid 10001,
the owner of the server's files. Nothing is deleted: the current database and pictures are moved aside.
Do not run it around 03:30, when the nightly backup runs.
cd /opt/kvitt
B="docker compose exec -T backup sh -c"
FOLDER=$(sed -n 's/^RCLONE_FOLDER_ID=//p' .env); REMOTE=$(sed -n 's/^RCLONE_REMOTE=//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:
$B 'c=$(mktemp); cp /config/rclone/rclone.conf "$c"; RCLONE_CONFIG=$c rclone lsf "$1" --drive-root-folder-id "$2" --files-only; rm -f "$c"' sh "$REMOTE" "$FOLDER" | sort
STAMP=20261102T013000Z # the stamp of the pair you chose
Download both files into the backups volume:
$B 'set -e; mkdir -p /backups/restore; c=$(mktemp); cp /config/rclone/rclone.conf "$c"
for f in "kvitt-$3.sqlite.gz" "pictures-$3.tar.gz"; do
RCLONE_CONFIG=$c rclone copy "$1$f" /backups/restore/ --drive-root-folder-id "$2"
done; rm -f "$c"; ls -l /backups/restore' sh "$REMOTE" "$FOLDER" "$STAMP"
(A file downloaded in the browser instead: copy it to the Pi with scp, then
docker compose cp <file> backup:/backups/restore/.)
3. Unpack and check
$B 'set -e; cd /backups/restore; gunzip -f "kvitt-$1.sqlite.gz"; sqlite3 "kvitt-$1.sqlite" "PRAGMA integrity_check"' sh "$STAMP"
It must print ok. Anything else: stop here and pick another backup.
4. Move the current data aside and put the backup in place
$B 'set -e; 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-$1.sqlite" /data/kvitt.sqlite
tar -xzf "/backups/restore/pictures-$1.tar.gz" -C /data
echo "previous data kept in $aside"; ls -l /data' sh "$STAMP"
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 --resolve icehack.asuscomm.com:8443:127.0.0.1 https://icehack.asuscomm.com:8443/health
The last line prints {"status":"ok",...}.
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:
$B 'rm -r /backups/restore'. The /data/pre-restore-<stamp>/ folder stays until you decide otherwise
($B 'ls /data' shows it). Run /opt/kvitt/backup-now.sh once to put the restored state on Drive.