c885aba313
infra/dev: compose stack on the Mac (server kvitt-server:dev, on-demand backup), .env.example, build/up/logs/backup/copy-google-services scripts. No Caddy, no TLS (debug builds accept http to private LAN addresses, AC-2.8). Bind-mounted data. infra/backup: alpine 3.24.2 image and backup.sh (SPEC 10.9; Drive upload skipped when RCLONE_FOLDER_ID is empty). infra/README.md for the dev stack and Firebase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
7.7 KiB
Markdown
144 lines
7.7 KiB
Markdown
# 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.
|
|
|
|
## Layout
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `dev/compose.yaml` | Dev stack: `server` (image `kvitt-server:dev`) and the on-demand `backup` job |
|
|
| `dev/.env.example` | Dev settings, no secret values; copy to `dev/.env` (git-ignored) to override |
|
|
| `dev/build.sh` | Builds `kvitt-server:dev` from `server/Dockerfile` and `kvitt-backup:dev` from `backup/Dockerfile` |
|
|
| `dev/up.sh` | Starts the server, waits for `/health`, prints the Mac and phone URLs; `--down` stops the stack |
|
|
| `dev/logs.sh` | Follows the logs (`dev/logs.sh --tail 100 server`) |
|
|
| `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 |
|
|
|
|
All commands below run from the repository root.
|
|
|
|
## Development stack
|
|
|
|
**Choices.** No Caddy and no TLS: debug builds accept `http://` to private LAN addresses (AC-2.8), so the server
|
|
publishes its own port (default 8080) on all of the Mac's interfaces. The server keeps the hardening of SPEC §8.9
|
|
(uid 10001, read-only root, no capabilities, `no-new-privileges`), the 160 MiB limit, `json-file` logging
|
|
(10 MB x 3) and `TZ=Europe/Stockholm`. Data lives in the bind-mounted folder `infra/dev/data/` (git-ignored) so it
|
|
is easy to inspect and reset. The secrets folder is mounted read-only at `/secrets`; if
|
|
`fcm-service-account.json` is missing the server runs with push disabled (§10.4).
|
|
|
|
On this Mac `docker` is Podman (`docker compose` runs `podman-compose`); the files work with Docker Desktop as well.
|
|
|
|
### Build
|
|
|
|
```bash
|
|
infra/dev/build.sh # both images
|
|
infra/dev/build.sh server # only kvitt-server:dev; needs server/Dockerfile (milestone M2a)
|
|
infra/dev/build.sh backup # only kvitt-backup:dev
|
|
```
|
|
|
|
The server build is `docker build --platform linux/arm64 -f server/Dockerfile -t kvitt-server:dev server`
|
|
(override the context with `KVITT_SERVER_BUILD_CONTEXT`). The script refuses to run while a Gradle build holds
|
|
`/tmp/kvitt-gradle.lock` (CLAUDE.md).
|
|
|
|
### Start, stop, logs
|
|
|
|
```bash
|
|
infra/dev/up.sh # start; prints health, the Mac URL and the phone URL
|
|
infra/dev/logs.sh # follow logs (Ctrl-C to stop following)
|
|
infra/dev/up.sh --down # stop and remove the containers (data stays in infra/dev/data/)
|
|
```
|
|
|
|
`KVITT_PUBLIC_URL` (used in invite links) is left empty in `.env.example`; `up.sh` sets it to
|
|
`http://<Mac LAN address>:<port>`. Pin it in `infra/dev/.env` if the Mac's address changes often.
|
|
|
|
### Finding the Mac's LAN address
|
|
|
|
`up.sh` prints it. By hand: `ipconfig getifaddr en0` (Wi-Fi on most Macs), or the interface of the default route:
|
|
`route -n get default | grep interface`, then `ipconfig getifaddr <interface>`. It must be a private address
|
|
(`192.168.*`, `10.*`, `172.16.*` to `172.31.*`) for a debug build to accept `http://` (AC-2.8).
|
|
|
|
The first time a phone connects, macOS may ask whether the container runtime (Podman's `gvproxy`, or Docker)
|
|
may accept incoming network connections: answer Allow.
|
|
|
|
### Bootstrap and pairing a debug build
|
|
|
|
The `bootstrap` and `pair-device` subcommands (SPEC §10.4, AC-1.1, AC-1.6) arrive with the server milestones.
|
|
|
|
```bash
|
|
C="docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example" # or --env-file infra/dev/.env
|
|
$C exec server kvitt-server bootstrap # first admin invite: code, link, expiry
|
|
$C exec server kvitt-server pair-device --user <user_id> # extra device invite for an existing user
|
|
$C run --rm server migrate # when the server is stopped (entrypoint is kvitt-server)
|
|
```
|
|
|
|
On the phone (home Wi-Fi, debug build `se.hagstedt.kvitt.debug`): open the pairing screen, enter the server URL
|
|
`http://<Mac LAN address>:8080` and the printed code (AC-2.7). Check reachability first by opening
|
|
`http://<Mac LAN address>:8080/health` in Chrome on the phone: it shows `"status":"ok"`.
|
|
|
|
### Backup (local, no Drive upload)
|
|
|
|
```bash
|
|
infra/dev/backup.sh
|
|
```
|
|
|
|
Runs `infra/backup/backup.sh` once in the `backup` image (compose profile `backup`, so `up` never starts it) with
|
|
`RCLONE_FOLDER_ID` empty: `.backup` snapshot, `PRAGMA integrity_check` must print `ok`, then
|
|
`infra/dev/backups/kvitt-<UTC stamp>.sqlite.gz`, `pictures-<stamp>.tar.gz` and `last-success`; the Drive upload
|
|
is skipped with a log line and the files are kept (SPEC §10.9, D-001). The wrapper then checks the new snapshot
|
|
again. To check by hand: `gunzip -c infra/dev/backups/kvitt-<stamp>.sqlite.gz > /tmp/k.sqlite && sqlite3 /tmp/k.sqlite "PRAGMA integrity_check"`.
|
|
|
|
### Resetting the dev data
|
|
|
|
```bash
|
|
infra/dev/up.sh --down
|
|
rm -rf infra/dev/data # the dev database, pictures and APKs; never the phones' data
|
|
infra/dev/up.sh # fresh database; run bootstrap again
|
|
```
|
|
|
|
Paired debug builds then hold a token the new database does not know: re-pair them (clear the debug app's data on
|
|
the phone, or reinstall the debug build).
|
|
|
|
### Worktrees
|
|
|
|
Agents work in `.claude/worktrees/agent-<id>`, which has no `infra/secrets/` unless copied (CLAUDE.md). Either copy
|
|
it from the main checkout, or set `KVITT_SECRETS_DIR=/Users/jhag/projects/kvitt/infra/secrets` in `infra/dev/.env`.
|
|
`infra/dev/copy-google-services.sh /Users/jhag/projects/kvitt` copies the Firebase config from the main checkout
|
|
into the worktree's `android/app/`.
|
|
|
|
## Firebase (SPEC §10.7)
|
|
|
|
Project `kvitt-1337` (Firebase added to the existing Google Cloud project). Free tier only; nothing billable is
|
|
created by this setup.
|
|
|
|
| Resource | Name |
|
|
|---|---|
|
|
| APIs enabled | `firebase.googleapis.com`, `fcm.googleapis.com`, `cloudresourcemanager.googleapis.com`, `iam.googleapis.com` |
|
|
| Android app | "Kvitt", package `se.hagstedt.kvitt` |
|
|
| Android app | "Kvitt debug", package `se.hagstedt.kvitt.debug` |
|
|
| Service account | `kvitt-fcm@kvitt-1337.iam.gserviceaccount.com` ("Kvitt FCM sender"), role `roles/firebasecloudmessaging.admin` (the narrowest predefined role holding `cloudmessaging.messages.create`) |
|
|
| Key | `infra/secrets/fcm-service-account.json` (git-ignored, mode 600) |
|
|
| Config | `infra/secrets/google-services.json`, one file with both Android clients (git-ignored) |
|
|
|
|
Put the config where Gradle reads it: `infra/dev/copy-google-services.sh`.
|
|
|
|
The setup used only `gcloud` and the Firebase Management REST API. To repeat or inspect it:
|
|
|
|
```bash
|
|
T=$(gcloud auth print-access-token)
|
|
H=(-H "Authorization: Bearer $T" -H "x-goog-user-project: kvitt-1337")
|
|
API=https://firebase.googleapis.com/v1beta1/projects/kvitt-1337
|
|
curl -sS "${H[@]}" "$API/androidApps" # list the apps (ids, packages)
|
|
curl -sS "${H[@]}" "$API/androidApps/<appId>/config" # configFileContents is base64 google-services.json
|
|
gcloud iam service-accounts keys list --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com --project kvitt-1337
|
|
```
|
|
|
|
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).
|