Owner checklist without the SSH add-on step: KVITT_PUBLIC_HOST placeholder, NPM's Details, SSL and Advanced tab fields, the Custom Nginx Configuration block (security headers of D-117 at server level, its own location / so NPM's X-Served-By does not drop them, /i/ kept out of NPM's access and error logs), what NPM logs, the HSTS caveat, the router rule external 8443 to the Home Assistant Pi's 443, the NPM-down fallback (no LAN path for release builds), operations and troubleshooting. RESTORE checks port 8080. Rehearsal of 2026-10-19 recorded. 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 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:
$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.