M2c-pi: Pi bundle (infra/pi), cert-sync, Drive backup upload, Gitea workflow
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
This commit is contained in:
+311
-8
@@ -1,9 +1,9 @@
|
||||
# infra: Kvitt infrastructure
|
||||
|
||||
This file covers the **development stack on the owner's Mac** (`infra/dev/`, milestone M2c-dev, D-001) and the
|
||||
**Firebase setup** (SPEC §10.7). The Pi stack (`infra/compose.yaml`, Caddy with the Let's Encrypt certificate,
|
||||
cert-sync, the Gitea registry, Drive backups, deploy and APK publishing) is built by M2c-pi, which adds its own
|
||||
sections here.
|
||||
This file covers the **development stack on the owner's Mac** (`infra/dev/`, milestone M2c-dev, D-001), the
|
||||
**Firebase setup** (SPEC §10.7) and the **Pi stack** (`infra/pi/`, milestone M2c-pi, D-100): a self-contained
|
||||
bundle the owner copies to the Kvitt Pi and runs there. No agent and no Mac script connects to the Pis; the owner
|
||||
runs every Pi command from the checklist in "Pi: what the owner does".
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -17,10 +17,16 @@ sections here.
|
||||
| `dev/backup.sh` | Runs the backup job once and checks the snapshot with `PRAGMA integrity_check` |
|
||||
| `dev/copy-google-services.sh` | Copies `secrets/google-services.json` to `android/app/google-services.json` |
|
||||
| `dev/lib.sh` | Helpers sourced by the scripts |
|
||||
| `backup/Dockerfile`, `backup/backup.sh` | Backup image and job of SPEC §10.9 (shared with the Pi stack) |
|
||||
| `secrets/` | Git-ignored: `fcm-service-account.json`, `google-services.json`. Never committed |
|
||||
| `backup/Dockerfile`, `backup/backup.sh`, `backup/healthcheck.sh` | Backup image and job of SPEC §10.9 (shared with the Pi stack); Drive upload and D-002 deletion |
|
||||
| `cert-sync/Dockerfile`, `cert-sync/sync.sh`, `cert-sync/entrypoint.sh` | Certificate sync image of SPEC §10.5.1 |
|
||||
| `pi/` | **The Pi bundle** (copied to the Kvitt Pi): `compose.yaml`, `Caddyfile`, `.env.example`, `install.sh`, `deploy.sh`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`, `lib.sh`; git-ignored `pi/.env`, `pi/secrets/`, `pi/out/` |
|
||||
| `build-images.sh` | Mac fallback for the image build: linux/arm64, push to the Gitea registry |
|
||||
| `publish-apk.sh` | Mac: release APK, mapping and `latest.json` into `pi/out/apk/` for the owner to copy to the Pi |
|
||||
| `RESTORE.md` | Restore from a Drive backup, step by step (SPEC §10.9) |
|
||||
| `../.gitea/workflows/build-images.yaml` | Gitea Actions image build (the normal path) |
|
||||
| `secrets/` | Git-ignored: `fcm-service-account.json`, `google-services.json`, `ha_ssh_key(.pub)`, `rclone.conf`, optional `registry.env`. Never committed |
|
||||
|
||||
All commands below run from the repository root.
|
||||
All Mac commands below run from the repository root of the main checkout (`/Users/jhag/projects/kvitt`).
|
||||
|
||||
## Development stack
|
||||
|
||||
@@ -140,4 +146,301 @@ gcloud iam service-accounts keys list --iam-account kvitt-fcm@kvitt-1337.iam.gse
|
||||
Rotating the key: create a new one with
|
||||
`gcloud iam service-accounts keys create infra/secrets/fcm-service-account.json --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com --project kvitt-1337`,
|
||||
restart the server, then delete the old key id with `gcloud iam service-accounts keys delete <key id> ...`.
|
||||
Copying the key to the Pi is M2c-pi (§10.7 step 7).
|
||||
Copying the key to the Pi (§10.7 step 7, mode 0440, group 10001) is done by `pi/install.sh` ("Pi: what the owner does", step 6).
|
||||
|
||||
## Pi stack (SPEC §10.1 to §10.10, D-100)
|
||||
|
||||
### How it fits together
|
||||
|
||||
| Where | What runs | Who runs it |
|
||||
|---|---|---|
|
||||
| Gitea Actions (runner label `host`, a Raspberry Pi) | `.gitea/workflows/build-images.yaml`: builds `kvitt-server`, `kvitt-cert-sync`, `kvitt-backup` for linux/arm64, pushes them to `gitea.lan/jacobh2/...` tagged `vMAJOR.MINOR.PATCH` and `latest`, pushes the git tag | Gitea, on a push to `server/**`, `infra/cert-sync/**`, `infra/backup/**`, `.gitea/workflows/**`, or by hand (Actions, "Run workflow") |
|
||||
| Owner's Mac | `infra/build-images.sh [tag]` (fallback image build), `infra/publish-apk.sh` (release APK into `infra/pi/out/apk/`) | The owner (or an agent, for the APK build only) |
|
||||
| Kvitt Pi, `/opt/kvitt/` | `compose.yaml`, `Caddyfile`, `.env`, `secrets/`, `import/`, `apk/`, and the scripts `deploy.sh`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh` (copied there by `install.sh`) | The owner, in their own SSH session |
|
||||
| Home Assistant Pi | Its own certbot renews `icehack.asuscomm.com`; cert-sync reads the two PEM files over SSH with a dedicated key | Untouched apart from one `authorized_keys` entry |
|
||||
|
||||
Stack (`pi/compose.yaml`, project name `kvitt`): `caddy` (publishes 8443, the only published port), `server`,
|
||||
`cert-sync`, `backup`; volumes `data`, `certs`, `caddy_data`, `backups`; network `kvitt`; every service
|
||||
`restart: unless-stopped`, json-file logs 10 MB x 3, `TZ=Europe/Stockholm`. Images are
|
||||
`${KVITT_REGISTRY}/kvitt-server|kvitt-cert-sync|kvitt-backup:${KVITT_TAG}` with `KVITT_REGISTRY=gitea.lan/jacobh2`
|
||||
(D-104; the names the workflow pushes).
|
||||
|
||||
### Versions (resolved 2026-10-18)
|
||||
|
||||
| Component | Version | Source |
|
||||
|---|---|---|
|
||||
| Caddy | `caddy:2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af` (multi-arch index; has linux/arm64/v8) | Docker Hub official image; tag `2.11.7` = `2.11` = `latest`, pushed 2026-10-14 |
|
||||
| Alpine (cert-sync, backup) | `alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6` | Docker Hub official image; `3.24` = `latest` |
|
||||
| cert-sync packages (Alpine v3.24) | openssh-client-default 10.3_p1-r1, curl 8.22.0-r0, openssl 3.5.9-r0, supercronic 0.2.49-r1, tzdata 2026e-r0 | `apk policy` in the pinned image |
|
||||
| backup packages (Alpine v3.24) | sqlite 3.53.4, rclone 1.74.1, tar 1.35, tzdata 2026e (M2c-dev), supercronic 0.2.49-r1 | `apk policy` in the pinned image |
|
||||
| Docker Engine on the Pi | Docker's Debian apt repository, arm64 packages (`docker-ce` 5:29.8.2 current): `docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin` | docs.docker.com/engine/install/raspberry-pi-os/: that page is for 32-bit (armhf) only; 64-bit Raspberry Pi OS uses the Debian instructions (docs.docker.com/engine/install/debian/) |
|
||||
|
||||
**Why supercronic, not busybox crond (D-104).** Both sidecars run as non-root users (cert-sync uid 1000, backup
|
||||
uid 10001). Busybox crond started as a non-root user in alpine 3.24.2 never ran a job (tested 2026-10-18: an
|
||||
every-minute job produced no output in two minutes). supercronic is a cron for containers that runs jobs as the
|
||||
user it runs as and honours `TZ`; the schedules are unchanged (`0 4 * * *` cert-sync, `30 3 * * *` backup).
|
||||
|
||||
### Backup uid (D-105, SPEC §10.2 "M2c-pi reconciles the two")
|
||||
|
||||
`backup` runs as **uid 10001**, the server's uid, on the Pi as on the Mac: `sqlite3 .backup` on a WAL database
|
||||
must open (and write) `kvitt.sqlite-shm`, which the server creates as 10001 with mode 0644, so uid 1000 could not
|
||||
back up the live database without loosening the server's files. Therefore `/opt/kvitt/secrets/rclone.conf` is
|
||||
mode 0600 **owned by 10001:10001** (not 1000). cert-sync stays uid 1000 with `ha_ssh_key` and `ha_known_hosts`
|
||||
mode 0600 owned by 1000:1000 (ssh refuses a key not owned by its user or readable by others). rclone writes
|
||||
refreshed access tokens back to its config; the mounted file is read-only, so `backup.sh` works on a private copy
|
||||
in `/tmp` each run. `install.sh` gives the named volumes their owners once (`/data`, `/data/apk`, `/backups` to
|
||||
10001, `/certs` to 1000) with a one-off root container (`docker compose run --user 0:0 --cap-add CHOWN`). Caddy
|
||||
runs as root (official image) and reads the 0640 certificate files.
|
||||
|
||||
### What was checked on the Mac (2026-10-18)
|
||||
|
||||
* `shellcheck` on every `infra/**/*.sh`: clean.
|
||||
* `podman-compose -f infra/pi/compose.yaml --env-file <filled sample> config`: exit 0.
|
||||
* `cert-sync` and `backup` images built for linux/arm64 with Podman (not pushed).
|
||||
* End-to-end rehearsal with Podman: a stand-in Home Assistant sshd with the variant A key below, the cert-sync
|
||||
image as uid 1000 with `StrictHostKeyChecking=yes`, the pinned Caddy with `pi/Caddyfile` in front of a stand-in
|
||||
server. The first fetch installed both files (0640); `GET /health` through Caddy answered over TLS; a renewed
|
||||
certificate was installed and Caddy reloaded through its admin API without a restart; an unchanged certificate
|
||||
was left alone; an older certificate and a key that does not match the certificate were refused with the old pair
|
||||
kept; TLS 1.1 was refused. **Caddy logged the request headers with `"Authorization":["REDACTED"]` and the token
|
||||
never appeared in its log** (SPEC §10.5.2).
|
||||
* Backup rehearsal with a local rclone remote: upload, `lsf` confirmation by name and size, local deletion only
|
||||
after both files were confirmed (D-002); files kept and `last-success` unchanged when the upload failed; the
|
||||
leftovers uploaded by the next good run; healthcheck green on a fresh start and after a run, red at 26 h.
|
||||
* Not checked (needs the real hosts): the Home Assistant add-on's handling of `authorized_keys` options, the
|
||||
Gitea workflow (not yet run), pulls from `gitea.lan`, the router forward, Drive.
|
||||
|
||||
### Gitea Actions workflow (`.gitea/workflows/build-images.yaml`), not yet run
|
||||
|
||||
Same setup as the owner's working example: `runs-on: host`, `sh` steps, `apk add git docker-cli docker-cli-buildx`,
|
||||
`docker login gitea.lan` with the repository secret `kvitt_cicd` (created by the owner) and `${{ github.actor }}`,
|
||||
a buildx builder whose `buildkitd.toml` sets `http = true` for `gitea.lan` (created once and reused, so its cache
|
||||
survives), `actions/checkout@v4` with `set-safe-directory` and `fetch-depth: 0`, the version from the newest
|
||||
`v1.0.*` git tag plus one, and the git tag pushed after the build. That version is what `deploy.sh` takes.
|
||||
|
||||
**Risk: the runner is a Raspberry Pi 3 with 1 GB RAM.** The server image compiles about 232 Rust crates,
|
||||
aws-lc-sys's C code among them. The workflow passes `--build-arg CARGO_BUILD_JOBS=1`; it takes effect only once
|
||||
`server/Dockerfile` declares `ARG CARGO_BUILD_JOBS` in its build stage (requested from the server's owner; until
|
||||
then buildx warns about an unused build argument and cargo uses all four cores, which may run out of memory). The
|
||||
Dockerfile already keeps the cargo registry and target directory in BuildKit cache mounts, so only the first build
|
||||
is a full one. The first build's duration on the Pi is unknown (a cold arm64 build takes 48 s on the M4 Mac). If
|
||||
it fails for memory, add swap on the runner Pi, or build on the Mac with `infra/build-images.sh`.
|
||||
|
||||
### Mac fallback: `infra/build-images.sh [tag]`
|
||||
|
||||
Builds the same three images for linux/arm64 and pushes them when `infra/secrets/registry.env` exists
|
||||
(`KVITT_REGISTRY=gitea.lan/jacobh2`, optional `KVITT_REGISTRY_USER` and `KVITT_REGISTRY_TOKEN` with
|
||||
write:package); otherwise it builds locally and prints the exact push commands. Tag: the short git SHA by default,
|
||||
plus `latest`. Refuses a dirty tree and does not build while a Gradle build holds the gradle-serial lock. With
|
||||
Podman (this Mac) it pushes with `--tls-verify=false`; with Docker it creates a buildx builder that allows plain
|
||||
HTTP to the registry.
|
||||
|
||||
### Publishing an APK: `infra/publish-apk.sh`
|
||||
|
||||
Version rule (SPEC §10.6): `version_code` = `git rev-list --count HEAD`, `version_name` = `YYYY.MM.DD-<short sha>`
|
||||
of HEAD's commit date (Europe/Stockholm), passed as `-Pkvitt.versionCode` and `-Pkvitt.versionName`. The image
|
||||
tags of the Gitea workflow (`v1.0.N`) are separate and say nothing about the APK. The script refuses a dirty tree,
|
||||
builds `:app:assembleRelease` through `tools/gradle-serial.sh` (JDK 25 on `PATH`), refuses an unsigned APK, and
|
||||
writes into `infra/pi/out/apk/`: `kvitt-<version_code>.apk`, `kvitt-<version_code>.mapping.txt` (the R8 mapping,
|
||||
for reading stack traces; never served) and `latest.json` (temp file, then rename); it keeps the newest 5 APKs
|
||||
with their mappings. Then the owner, on the Mac:
|
||||
|
||||
```bash
|
||||
cd /Users/jhag/projects/kvitt
|
||||
scp infra/pi/out/apk/* <KVITT_PI_SSH_USER>@<KVITT_PI_HOST>:/opt/kvitt/apk/
|
||||
ssh <KVITT_PI_SSH_USER>@<KVITT_PI_HOST> /opt/kvitt/install-apk.sh
|
||||
```
|
||||
|
||||
`install-apk.sh` checks the SHA-256 against `latest.json`, copies the APKs into the `data` volume (`/data/apk/`)
|
||||
and `latest.json` last with a rename, keeps the newest 5 there and prints `GET /apk/latest`.
|
||||
|
||||
## Pi: what the owner does
|
||||
|
||||
Fill these in first (in `infra/pi/.env` on the Mac; the bundle carries the file to the Pi):
|
||||
|
||||
| Placeholder in `infra/pi/.env` | What | Where to find it |
|
||||
|---|---|---|
|
||||
| `KVITT_PI_HOST` | LAN address of the Kvitt Pi (give it a static DHCP lease) | ASUS router web UI, LAN, DHCP Server, client list |
|
||||
| `KVITT_PI_SSH_USER` | Your user on the Kvitt Pi | The user set in Raspberry Pi Imager |
|
||||
| `HA_HOST` | LAN address of the Home Assistant Pi | Router client list, or Home Assistant, Settings, System, Network |
|
||||
| `HA_PORT` | SSH port of the SSH add-on (default 22) | Home Assistant, Settings, Add-ons, the SSH add-on, Configuration, Network |
|
||||
| `HA_USER` | SSH user of the add-on (`root` for "Terminal & SSH"; the configured `username` for "Advanced SSH & Web Terminal") | Same add-on, Configuration |
|
||||
| `HA_CERT_DIR` | Directory holding `fullchain.pem` and `privkey.pem` on the Home Assistant Pi (`/ssl` on Home Assistant OS) | In the add-on's terminal: `ls -l /ssl` |
|
||||
| `KVITT_TAG` | Image tag to run: the newest `v1.0.N` the Gitea workflow pushed (or the SHA from `build-images.sh`) | Gitea, the repository, Packages, `kvitt-server` |
|
||||
| `KVITT_REGISTRY_USER`, `KVITT_REGISTRY_TOKEN` | Only if the Gitea packages are private: a Gitea user and a token with `read:package` | Gitea, Settings, Applications, Generate New Token |
|
||||
| `KVITT_REGISTRY` | Already `gitea.lan/jacobh2` | |
|
||||
| `RCLONE_REMOTE`, `RCLONE_FOLDER_ID` | Already `gdrive:` and `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV` | |
|
||||
|
||||
### 1. Provision the Kvitt Pi
|
||||
|
||||
Raspberry Pi Imager on the Mac: Raspberry Pi 3, **Raspberry Pi OS Lite (64-bit)**; in its settings set the host
|
||||
name, your user and password, the network, time zone Europe/Stockholm, and enable SSH with your public key. Boot
|
||||
it, give it a static DHCP lease in the router, then from the Mac: `ssh <KVITT_PI_SSH_USER>@<KVITT_PI_HOST>`.
|
||||
|
||||
### 2. Install Docker Engine on the Pi (docs.docker.com, Debian arm64 repository)
|
||||
|
||||
On the Pi:
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt install -y ca-certificates curl python3
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
|
||||
Types: deb
|
||||
URIs: https://download.docker.com/linux/debian
|
||||
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
|
||||
Components: stable
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/docker.asc
|
||||
EOF
|
||||
sudo apt update
|
||||
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
sudo usermod -aG docker "$USER"
|
||||
exit
|
||||
```
|
||||
|
||||
Log in again; `docker compose version` prints a version and `docker run --rm hello-world` prints
|
||||
"Hello from Docker!".
|
||||
|
||||
### 3. Put the cert-sync key on the Home Assistant SSH add-on
|
||||
|
||||
Home Assistant, Settings, Add-ons, your SSH add-on, Configuration, `authorized_keys`: add **one** new entry.
|
||||
Try variant A first (replace `/ssl` if your `HA_CERT_DIR` differs):
|
||||
|
||||
Variant A (restricted, preferred):
|
||||
|
||||
```
|
||||
restrict,command="tar -C /ssl -cf - fullchain.pem privkey.pem" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMiXj2dU8GU0Xmdj91WxLhVpPOI1zRWvBF5h2MbE4S8V kvitt-cert-sync
|
||||
```
|
||||
|
||||
Save, restart the add-on. If the add-on refuses to save or start with it (its Log tab shows an error about the
|
||||
key), replace it with variant B:
|
||||
|
||||
Variant B (plain key):
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMiXj2dU8GU0Xmdj91WxLhVpPOI1zRWvBF5h2MbE4S8V kvitt-cert-sync
|
||||
```
|
||||
|
||||
cert-sync works with both unchanged: it always asks for exactly that `tar` command. Tell the orchestrator which
|
||||
variant the add-on accepted, so it is recorded here. The private key is
|
||||
`/Users/jhag/projects/kvitt/infra/secrets/ha_ssh_key` (git-ignored; never paste it anywhere).
|
||||
|
||||
### 4. Fill the `.env` and stage the secrets, on the Mac
|
||||
|
||||
```bash
|
||||
cd /Users/jhag/projects/kvitt
|
||||
cp infra/pi/.env.example infra/pi/.env
|
||||
open -e infra/pi/.env # replace every CHANGE_ME (table above), save
|
||||
mkdir -p infra/pi/secrets
|
||||
cp infra/secrets/fcm-service-account.json infra/secrets/ha_ssh_key infra/secrets/rclone.conf infra/pi/secrets/
|
||||
```
|
||||
|
||||
`rclone.conf` is already generated from your `rclone authorize "drive"` (remote `[gdrive]`, `type = drive`,
|
||||
`scope = drive`, the token); the Drive folder id is `RCLONE_FOLDER_ID` in `.env`, not in the conf.
|
||||
`infra/pi/.env` and `infra/pi/secrets/` are git-ignored.
|
||||
|
||||
### 5. Copy the bundle to the Pi and record the Home Assistant host key
|
||||
|
||||
On the Mac:
|
||||
|
||||
```bash
|
||||
scp -r infra/pi <KVITT_PI_SSH_USER>@<KVITT_PI_HOST>:kvitt-pi
|
||||
rm -r infra/pi/secrets # the originals stay in infra/secrets/
|
||||
```
|
||||
|
||||
On the Pi:
|
||||
|
||||
```bash
|
||||
cd ~/kvitt-pi
|
||||
ssh-keyscan -p <HA_PORT> <HA_HOST> > secrets/ha_known_hosts
|
||||
wc -l secrets/ha_known_hosts # at least 1 line
|
||||
```
|
||||
|
||||
### 6. Install and start
|
||||
|
||||
On the Pi:
|
||||
|
||||
```bash
|
||||
cd ~/kvitt-pi && ./install.sh
|
||||
```
|
||||
|
||||
It creates `/opt/kvitt/{secrets,import,apk}`, installs the four secret files (`fcm-service-account.json` 0440
|
||||
root:10001; `ha_ssh_key`, `ha_known_hosts` 0600 1000:1000; `rclone.conf` 0600 10001:10001), adds `gitea.lan` to
|
||||
`insecure-registries` in `/etc/docker/daemon.json` (and restarts Docker if that changed), copies `compose.yaml`,
|
||||
`Caddyfile`, `.env` (0600) and the scripts to `/opt/kvitt`, pulls, starts and waits. Expected end:
|
||||
`server: healthy`, `cert-sync: healthy`, `caddy: healthy`, a `docker compose ps` table, the line
|
||||
`GET https://icehack.asuscomm.com:8443/health (via this Pi): {"status":"ok",...}`, then "Kvitt is running.".
|
||||
Then `rm -r ~/kvitt-pi/secrets` (the script reminds you). Running `install.sh` again is safe.
|
||||
|
||||
### 7. Router forward
|
||||
|
||||
ASUS router web UI, WAN, Virtual Server / Port Forwarding: add a rule, protocol TCP, external port 8443, internal
|
||||
IP `<KVITT_PI_HOST>`, internal port 8443; Apply. Then on the phone with Wi-Fi off, open
|
||||
`https://icehack.asuscomm.com:8443/health` in Chrome: it shows `"status":"ok"` without a certificate warning (yes/no).
|
||||
|
||||
### 8. First backup and bootstrap
|
||||
|
||||
On the Pi:
|
||||
|
||||
```bash
|
||||
/opt/kvitt/backup-now.sh # ends with "last-success: <stamp>" and the newest kvitt-*.sqlite.gz and pictures-*.tar.gz on Drive
|
||||
/opt/kvitt/bootstrap.sh # first install only: prints the admin invite code, link and expiry
|
||||
```
|
||||
|
||||
### 9. Deploying a new version
|
||||
|
||||
After the Gitea workflow pushed `v1.0.N` (or `infra/build-images.sh` printed a tag), on the Pi:
|
||||
|
||||
```bash
|
||||
/opt/kvitt/deploy.sh v1.0.N
|
||||
```
|
||||
|
||||
It backs up first (Drive confirmed) on every deploy, because any tag may carry a migration (SPEC §10.10), sets
|
||||
`KVITT_TAG`, pulls, starts, waits for `server: healthy` and prints `/health`. If the server does not come up:
|
||||
`/opt/kvitt/deploy.sh "$(cat /opt/kvitt/previous-tag)" --skip-backup`; if a migration had partially applied,
|
||||
restore the pre-deploy backup (`infra/RESTORE.md`). A newer bundle with a changed `compose.yaml` or `Caddyfile` is
|
||||
installed by copying the bundle again (step 5, without the secrets) and running `./install.sh`.
|
||||
|
||||
### Fallback: Caddy internal CA (SPEC §10.5.3, not deployed)
|
||||
|
||||
Only if cert-sync cannot be made to work. In `/opt/kvitt/Caddyfile` replace the line
|
||||
`tls /certs/fullchain.pem /certs/privkey.pem` with `tls internal`, then `docker compose restart caddy` in
|
||||
`/opt/kvitt`. Copy Caddy's root to the Mac once: on the Pi
|
||||
`docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/kvitt_root_ca.crt`, then on the Mac
|
||||
`scp <KVITT_PI_SSH_USER>@<KVITT_PI_HOST>:/tmp/kvitt_root_ca.crt android/app/src/release/res/raw/kvitt_root_ca.crt`
|
||||
(a public certificate, committed by an Android agent) and set `kvitt.tls.pinnedRoot=true` in
|
||||
`android/gradle.properties` (D-082). The root is valid 10 years. Caddy still waits for cert-sync to be healthy,
|
||||
which needs the two files in the `certs` volume: keep the last fetched pair there, or remove the `cert-sync`
|
||||
condition from `depends_on` in `/opt/kvitt/compose.yaml`.
|
||||
|
||||
### Operations (SPEC §10.10), on the Pi in `/opt/kvitt`
|
||||
|
||||
| Task | Command |
|
||||
|---|---|
|
||||
| Status | `docker compose ps` |
|
||||
| Logs | `docker compose logs --tail 100 <caddy, server, cert-sync or backup>` |
|
||||
| Backup now | `/opt/kvitt/backup-now.sh` |
|
||||
| Upgrade | `/opt/kvitt/deploy.sh <tag>` |
|
||||
| Roll back | `/opt/kvitt/deploy.sh "$(cat /opt/kvitt/previous-tag)" --skip-backup` |
|
||||
| Certificate now | `docker compose exec cert-sync /usr/local/bin/sync.sh` |
|
||||
| Certificate expiry and issuer | `docker compose exec cert-sync openssl x509 -in /certs/fullchain.pem -noout -enddate -issuer` |
|
||||
| Extra device invite | `docker compose exec server kvitt-server pair-device --user <user_id>` |
|
||||
| Memory | `docker stats --no-stream` (server RSS under 100 MB with both phones syncing) |
|
||||
| Disk | `df -h /` and `docker system df`; SQLite a few MB, pictures under 1 MB, APKs about 20 MB each, 5 kept |
|
||||
| No token in Caddy's log | `docker compose logs caddy \| grep -c 'kvt_'` prints `0` |
|
||||
| Restore | `infra/RESTORE.md` |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
* `cert-sync` unhealthy, so `caddy` does not start: `docker compose logs cert-sync`. "Host key verification
|
||||
failed": redo `ssh-keyscan` (step 5) and run `./install.sh` again. "Permission denied (publickey)": the add-on
|
||||
does not have the key (step 3). "did not hold fullchain.pem and privkey.pem": wrong `HA_CERT_DIR`, or `HA_USER`
|
||||
cannot read `privkey.pem`.
|
||||
* Pull fails with "http: server gave HTTP response to HTTPS client": `gitea.lan` is missing from
|
||||
`/etc/docker/daemon.json` (`install.sh` adds it; check with `docker info | grep -A3 'Insecure Registries'`).
|
||||
* Pull fails with "unauthorized": the packages are private; set `KVITT_REGISTRY_USER` and `KVITT_REGISTRY_TOKEN`
|
||||
in `~/kvitt-pi/.env` and run `./install.sh` again.
|
||||
* `install.sh` says `/opt/kvitt/secrets/<file>` is a directory: Docker created it because the file was missing at a
|
||||
start; `sudo rmdir` it, put the file in `~/kvitt-pi/secrets/`, run `./install.sh`.
|
||||
|
||||
Reference in New Issue
Block a user