SPEC v0.37: image builds on the Mac, v1.0.3 deployed, web client verified (D-211)

infra/build-images.sh on the Mac with Podman is the normal image build, the
Gitea workflow on the Pi 3 runner the fallback (revises D-156 point 5); tag
rule, registry port 80 from the Mac (the Pi's .env keeps gitea.lan:3001),
Podman insecure-registry files, the script fixes f15fa23 and 8fa9198.
v1.0.3 on the Kvitt Pi; the owner verified the web client from the laptop
browser and S-11's Insights (SPEC §10.3, §10.10, §11.2 to §11.4, §15;
infra/README.md; HANDOFF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-24 13:14:00 +02:00
parent 8b620c8c6a
commit ae73f58079
4 changed files with 83 additions and 52 deletions
+52 -19
View File
@@ -22,10 +22,10 @@ from the checklist in "Pi: what the owner does".
| `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 |
| `pi/` | **The Pi project** (copied to `~/kvitt` on the Kvitt Pi): `compose.yaml`, `.env.example`; git-ignored `pi/.env`, `pi/secrets/`, `pi/out/` |
| `npm/kvitt.conf` | The complete NPM Custom Nginx Configuration block of step 4, as a file to open and paste (placeholder `KVITT_APP_KEY`) |
| `build-images.sh` | Mac fallback for the image build: linux/arm64, push to the Gitea registry |
| `build-images.sh` | The image build (the normal path, D-211): on the Mac with Podman, 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) |
| `../.gitea/workflows/build-images.yaml` | Gitea Actions image build on the Pi 3 runner (the fallback since D-211) |
| `secrets/` | Git-ignored: `fcm-service-account.json`, `google-services.json`, `rclone.conf`, optional `registry.env` (and `ha_ssh_key(.pub)`, unused since D-120). Never committed |
All Mac commands below run from the repository root of the main checkout (`/Users/jhag/projects/kvitt`).
@@ -163,8 +163,8 @@ The key goes to the Pi as `~/kvitt/secrets/fcm-service-account.json` ("Pi: what
| Where | What runs | Who runs it |
|---|---|---|
| Gitea Actions (runner label `host`, a Raspberry Pi) | `.gitea/workflows/build-images.yaml`: builds `kvitt-server` and `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/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) |
| Gitea Actions (runner label `host`, a Raspberry Pi; the fallback since D-211) | `.gitea/workflows/build-images.yaml`: builds `kvitt-server` and `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/backup/**`, `.gitea/workflows/**`, or by hand (Actions, "Run workflow") |
| Owner's Mac | `infra/build-images.sh v1.0.N` (the image build, the normal path, D-211), `infra/publish-apk.sh` (release APK into `infra/pi/out/apk/`) | The owner (or an agent, for the APK build only) |
| Kvitt Pi, `/home/jacobhagstedt/kvitt/` | `compose.yaml`, `.env`, `secrets/`, and the bind-mounted `data/` (SQLite, pictures, `apk/`), `backups/`, `import/`; nothing outside this folder (D-125) | The owner, in their own SSH session, with plain `docker compose` commands |
| Home Assistant Pi | Nginx Proxy Manager (NPM): a proxy host for `<KVITT_PUBLIC_HOST>` with its own Let's Encrypt certificate, TLS on 443, plain HTTP to `<kvitt-pi-address>:8080` over the LAN (D-120); the app gate closes every request from outside the LAN without the app key (D-126) | The owner, in NPM's web UI (steps 4 and 4a) |
| Router (ASUS) | No new rule: the existing external TCP 443 forward to the Home Assistant Pi (NPM, for Home Assistant) serves Kvitt too, NPM picks the proxy host by name; external 80 already reaches NPM for Let's Encrypt; nothing is forwarded to the Kvitt Pi (D-124) | The owner checks it (step 5) |
@@ -265,7 +265,11 @@ confirmation by name and size, local deletion only after both files were confirm
`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. (The Caddy and cert-sync rehearsals of that day concern the removed stack.)
### Gitea Actions workflow (`.gitea/workflows/build-images.yaml`)
### Gitea Actions workflow (`.gitea/workflows/build-images.yaml`), the fallback
Since 2026-10-24 (D-211) images are built on the Mac (next section); this workflow on the Pi 3 runner stays as the
fallback (about three hours per run, and the runner's disk fills). A push that touches its paths queues a run; the
owner cancels it in Gitea (the repository, Actions) when the Mac build is used.
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 `kvitt_cicd` (a user-level secret of the repository owner, created by the owner;
@@ -298,14 +302,41 @@ keeps the cargo registry and target directory in BuildKit cache mounts, so only
takes 48 s on the M4 Mac); later runs reuse the builder's cargo cache. If a build 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]`
### Image build on the Mac: `infra/build-images.sh v1.0.N` (the normal path, D-211)
Builds the same two 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.
Builds the two images for linux/arm64 and pushes them when `infra/secrets/registry.env` exists
(`KVITT_REGISTRY=gitea.lan/jacobh2`, `KVITT_REGISTRY_USER` and `KVITT_REGISTRY_TOKEN` with write:package);
otherwise it builds locally and prints the exact push commands. The server build gets
`KVITT_VERSION_CODE=<git rev-list --count HEAD>` as in the workflow (`f15fa23`). 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. The engine test captures
`docker version` before matching it (`8fa9198`: a `grep -q` pipe under `pipefail` made it pick docker).
```bash
cd /Users/jhag/projects/kvitt
git fetch --tags
git tag --sort=-v:refname | head -1 # highest tag on origin, for example v1.0.3
infra/build-images.sh v1.0.4 # the next v1.0.N: kvitt-server and kvitt-backup, tagged v1.0.4 and latest
git tag v1.0.4 && git push origin v1.0.4 # the same tag on the built commit, so the workflow's numbering continues
```
Tag rule: the next `v1.0.N` after the highest `v1.0.*` tag on `origin` (`v1.0.3` = `8fa9198`, the first Mac
build). The script ends with the deploy hint for the Pi (step 7). It prints no image digests; the digest comparison
under Operations applies to workflow builds.
**Registry port.** From the Mac the registry is `gitea.lan` on port 80 (plain HTTP), so `registry.env` says
`KVITT_REGISTRY=gitea.lan/jacobh2`. The Pi's own `.env` keeps `KVITT_REGISTRY=gitea.lan:3001/jacobh2`, the port it
reaches the same registry on locally; both name the same images.
**Podman and the plain-HTTP registry** (owner's allowance 2026-10-24, outside the repository). Podman refuses
`gitea.lan` until it is marked insecure, in both places (the Mac and the Podman machine, where builds and pushes
run). Both files are in place since 2026-10-24; repeat this only on a new Mac or a recreated Podman machine:
```bash
mkdir -p ~/.config/containers/registries.conf.d
printf '[[registry]]\nlocation = "gitea.lan"\ninsecure = true\n' > ~/.config/containers/registries.conf.d/kvitt.conf
podman machine ssh "sudo mkdir -p /etc/containers/registries.conf.d && printf '[[registry]]\nlocation = \"gitea.lan\"\ninsecure = true\n' | sudo tee /etc/containers/registries.conf.d/kvitt.conf"
```
### Publishing an APK: `infra/publish-apk.sh`
@@ -364,8 +395,9 @@ folders `data/`, `backups/`, `import/`. Nothing outside that folder is installed
the Kvitt Pi's LAN IPv4 address (static DHCP lease; ASUS router web UI, LAN, DHCP Server, client list), also NPM's
Forward Hostname / IP (step 4).
Between two workflow builds, run `docker builder prune -af` on the runner Pi: its 15 GB disk reached 11 GB during
builds (SPEC §10.3, D-156). On the Samsung phones, turn Auto Blocker off before tapping "Uppdatera" for an in-app
Images are built on the Mac (`infra/build-images.sh v1.0.N`, "Image build on the Mac" above, D-211). If the
fallback workflow runs instead, run `docker builder prune -af` on the runner Pi between two of its builds: its 15 GB
disk reached 11 GB during builds (SPEC §10.3, D-156). On the Samsung phones, turn Auto Blocker off before tapping "Uppdatera" for an in-app
update and back on afterwards; with it on, the install is refused (SPEC AC-16.3, D-156).
### 1. Prerequisites (already on the Pi)
@@ -394,7 +426,7 @@ rm -r infra/pi/secrets # the originals stay in infra/secrets/
|---|---|
| `HA_HOST` | LAN IPv4 address of the Home Assistant Pi (NPM), for example `192.168.1.20`, not `homeassistant.local` (router client list, or Home Assistant, Settings, System, Network) |
| `KVITT_UID`, `KVITT_GID` | `id -u` and `id -g` on the Kvitt Pi as `jacobhagstedt` (default 1000, 1000) |
| `KVITT_TAG` | `v1.0.0`, or the newest `v1.0.N` the Gitea workflow pushed (Gitea, the repository, Packages, `kvitt-server`) |
| `KVITT_TAG` | The newest `v1.0.N` pushed (by `infra/build-images.sh` or the workflow; Gitea, the repository, Packages, `kvitt-server`) |
| `KVITT_PUBLIC_HOST`, `KVITT_REGISTRY`, `RCLONE_FOLDER_ID` | Already `kvitt.suorra.duckdns.org`, `gitea.lan/jacobh2`, `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV` |
`rclone.conf` is generated from your `rclone authorize "drive"` (remote `[gdrive]`, `scope = drive`); the Drive
@@ -670,7 +702,8 @@ itself opens the link anywhere. The partner's invite, once the admin is paired:
### 7. Deploying a new version
After the Gitea workflow pushed `v1.0.N` (or `infra/build-images.sh` printed a tag), on the Pi:
After `infra/build-images.sh v1.0.N` on the Mac pushed the images (the normal path, D-211; or the fallback workflow
pushed `v1.0.N`), on the Pi:
```bash
cd ~/kvitt
@@ -682,7 +715,7 @@ docker compose ps && curl -s localhost:8080/health
Continue only when the backup ended with `backup: ok`. The images come over plain HTTP from `gitea.lan` (D-001), so
compare the pulled digests (Operations, "Image digests") with the workflow's "Print image digests" step for that
run (D-117). Rollback: set the previous tag in `.env` the same way and run `docker compose pull && docker compose
run (D-117; a Mac build prints none). Rollback: set the previous tag in `.env` the same way and run `docker compose pull && docker compose
up -d`; if a migration had partially applied, restore the pre-deploy backup (`infra/RESTORE.md`).
### Fallback (SPEC §10.5.3): when NPM is down
@@ -740,8 +773,8 @@ or a leaked key (rotate it, step 4a).
* `mem_limit` (2026-10-24, D-156): the lines stay in `compose.yaml`; the Pi kernel without the memory cgroup ignores
them with a warning, accepted.
* Firewall rule limiting port 8080 to `HA_HOST` (2026-10-24, D-156): not now (deferred).
* Image builds (2026-10-24, D-156): stay on the Pi runner for now; `infra/build-images.sh` on the Mac remains the
fallback.
* Image builds (2026-10-24, D-211, revises D-156): on the Mac with `infra/build-images.sh v1.0.N`; the workflow on
the Pi runner is the fallback.
### Troubleshooting