Files
kvitt/infra/RESTORE.md
T
jacobh2 62f5279daa infra README: Nginx Proxy Manager proxy host, router to NPM, NPM fallback (D-120)
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
2026-10-19 19:50:28 +02:00

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.