M2c-dev: development compose stack, backup job, Firebase setup docs
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>
This commit is contained in:
@@ -27,3 +27,10 @@ infra/secrets/
|
||||
*service-account*.json
|
||||
google-services.json
|
||||
*.apk
|
||||
# Development stack on the Mac (infra/dev): local env, bind-mounted data and local backups
|
||||
infra/dev/.env
|
||||
infra/dev/data/
|
||||
infra/dev/backups/
|
||||
*.sqlite.gz
|
||||
*.sqlite-wal
|
||||
*.sqlite-shm
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# shellcheck settings for every script under infra/ (found by searching upward from each script).
|
||||
external-sources=true
|
||||
source-path=SCRIPTDIR
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,20 @@
|
||||
# Kvitt backup job image (SPEC §10.3, §10.9): alpine with sqlite, gzip, tar, rclone and backup.sh run by crond.
|
||||
# Versions resolved 2026-10-17: alpine 3.24.2 (Docker Hub official image, multi-arch index digest below);
|
||||
# packages from Alpine v3.24 main/community: sqlite 3.53.4, rclone 1.74.1, tar 1.35, tzdata 2026e.
|
||||
FROM docker.io/library/alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
|
||||
|
||||
RUN apk add --no-cache \
|
||||
"sqlite~3.53" \
|
||||
"rclone~1.74" \
|
||||
"tar~1.35" \
|
||||
gzip \
|
||||
tzdata
|
||||
|
||||
COPY backup.sh /backup.sh
|
||||
RUN chmod 0755 /backup.sh \
|
||||
&& echo '30 3 * * * /backup.sh' > /etc/crontabs/root
|
||||
|
||||
ENV TZ=Europe/Stockholm
|
||||
# Daily at 03:30 Europe/Stockholm. The development stack runs it on demand instead
|
||||
# (`docker compose run --rm backup /backup.sh`, see infra/README.md).
|
||||
CMD ["crond", "-f", "-l", "8"]
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
# Kvitt backup job (SPEC §10.9). Runs inside the backup image with /data (server data) and /backups mounted.
|
||||
# 1. online, consistent snapshot of /data/kvitt.sqlite with sqlite3 .backup
|
||||
# 2. PRAGMA integrity_check on the snapshot must print ok, else abort
|
||||
# 3. gzip -9 the snapshot and tar the pictures, both with the same UTC stamp
|
||||
# 4. Drive upload: skipped when RCLONE_FOLDER_ID is empty (the development stack, D-001);
|
||||
# the upload itself and the D-002 local deletion are built by M2c-pi
|
||||
# 5. write /backups/last-success with the stamp
|
||||
# Logs carry file names and sizes only, never database content (SPEC §8.7).
|
||||
set -eu
|
||||
|
||||
DATA_DIR=${KVITT_DATA_DIR:-/data}
|
||||
BACKUP_DIR=${BACKUP_DIR:-/backups}
|
||||
DB="$DATA_DIR/kvitt.sqlite"
|
||||
STAGING="$BACKUP_DIR/staging"
|
||||
|
||||
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) backup: $*"; }
|
||||
fail() { log "FAILED: $*"; rm -f "$STAGING/kvitt.sqlite"; exit 1; }
|
||||
|
||||
[ -f "$DB" ] || fail "no database at $DB"
|
||||
mkdir -p "$STAGING"
|
||||
rm -f "$STAGING/kvitt.sqlite"
|
||||
|
||||
# 1. Snapshot
|
||||
sqlite3 -batch "$DB" ".backup '$STAGING/kvitt.sqlite'" || fail "sqlite3 .backup returned an error"
|
||||
|
||||
# 2. Integrity check
|
||||
result=$(sqlite3 -batch -noheader -list "$STAGING/kvitt.sqlite" "PRAGMA integrity_check") || fail "integrity_check could not run"
|
||||
[ "$result" = "ok" ] || fail "integrity_check did not print ok"
|
||||
|
||||
# 3. Compress with one UTC stamp
|
||||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
db_out="$BACKUP_DIR/kvitt-$stamp.sqlite.gz"
|
||||
pics_out="$BACKUP_DIR/pictures-$stamp.tar.gz"
|
||||
gzip -9 -c "$STAGING/kvitt.sqlite" > "$db_out.tmp" || fail "gzip failed"
|
||||
mv "$db_out.tmp" "$db_out"
|
||||
rm -f "$STAGING/kvitt.sqlite"
|
||||
if [ -d "$DATA_DIR/pictures" ]; then
|
||||
tar -czf "$pics_out.tmp" -C "$DATA_DIR" pictures || fail "tar of pictures failed"
|
||||
else
|
||||
log "no pictures directory yet; writing an empty pictures archive"
|
||||
tar -czf "$pics_out.tmp" -T /dev/null || fail "tar of empty pictures archive failed"
|
||||
fi
|
||||
mv "$pics_out.tmp" "$pics_out"
|
||||
log "wrote $(basename "$db_out") ($(wc -c < "$db_out") bytes) and $(basename "$pics_out") ($(wc -c < "$pics_out") bytes)"
|
||||
|
||||
# 4. Drive upload
|
||||
if [ -z "${RCLONE_FOLDER_ID:-}" ]; then
|
||||
log "RCLONE_FOLDER_ID is empty: Drive upload skipped, local files kept"
|
||||
else
|
||||
# TODO(M2c-pi): rclone copy --immutable to "${RCLONE_REMOTE:-gdrive:}" --drive-root-folder-id, confirm with
|
||||
# rclone lsf, then delete the local files (D-002). Not part of the development stack (D-001).
|
||||
fail "RCLONE_FOLDER_ID is set but the Drive upload is not built yet (M2c-pi); local files kept"
|
||||
fi
|
||||
|
||||
# 5. Success marker
|
||||
echo "$stamp" > "$BACKUP_DIR/last-success.tmp"
|
||||
mv "$BACKUP_DIR/last-success.tmp" "$BACKUP_DIR/last-success"
|
||||
log "ok $stamp"
|
||||
@@ -0,0 +1,31 @@
|
||||
# Kvitt development stack settings (infra/dev/compose.yaml). No secret values here, ever.
|
||||
# Copy to infra/dev/.env (git-ignored) to change anything; the scripts use .env when it exists,
|
||||
# otherwise this file. Paths are relative to infra/dev/.
|
||||
|
||||
# Images built by infra/dev/build.sh.
|
||||
KVITT_SERVER_IMAGE=kvitt-server:dev
|
||||
KVITT_BACKUP_IMAGE=kvitt-backup:dev
|
||||
|
||||
# Port published on the Mac (all interfaces, so the phone on the home Wi-Fi can reach it).
|
||||
KVITT_DEV_PORT=8080
|
||||
|
||||
# URL used in invite links. Leave empty: up.sh sets http://<Mac LAN address>:<KVITT_DEV_PORT>.
|
||||
# Set it only to pin an address, for example http://192.168.1.20:8080 (private LAN address, plain http).
|
||||
KVITT_PUBLIC_URL=
|
||||
|
||||
# Firebase project for FCM HTTP v1 (SPEC §10.4, §10.7).
|
||||
KVITT_FCM_PROJECT_ID=kvitt-1337
|
||||
|
||||
# tracing filter (SPEC §10.4).
|
||||
RUST_LOG=info
|
||||
|
||||
# Bind-mounted folders (git-ignored). Deleting data/ resets the dev server.
|
||||
KVITT_DEV_DATA_DIR=./data
|
||||
KVITT_DEV_BACKUP_DIR=./backups
|
||||
|
||||
# Folder holding fcm-service-account.json (git-ignored). Default: infra/secrets of this checkout.
|
||||
# In a worktree without copied secrets, point it at the main checkout's infra/secrets.
|
||||
KVITT_SECRETS_DIR=../secrets
|
||||
|
||||
# rclone remote name; unused on the dev stack because RCLONE_FOLDER_ID is always empty here.
|
||||
RCLONE_REMOTE=gdrive:
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the backup job once against the development data (SPEC §10.9 with RCLONE_FOLDER_ID empty: no Drive upload).
|
||||
# Writes kvitt-<UTC stamp>.sqlite.gz and pictures-<stamp>.tar.gz into the backups folder (default infra/dev/backups/)
|
||||
# and checks the newest snapshot with PRAGMA integrity_check, using the backup image's own sqlite3.
|
||||
set -euo pipefail
|
||||
# shellcheck source=lib.sh
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
backup_dir="$(dev_path "$(env_value KVITT_DEV_BACKUP_DIR)")"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
compose --profile backup run --rm -T backup /backup.sh
|
||||
|
||||
latest="$(find "$backup_dir" -maxdepth 1 -name 'kvitt-*.sqlite.gz' | sort | tail -n 1)"
|
||||
[[ -n "$latest" ]] || { echo "backup.sh: no snapshot found in $backup_dir" >&2; exit 1; }
|
||||
name="$(basename "$latest")"
|
||||
result="$(compose --profile backup run --rm -T --entrypoint sh backup -c \
|
||||
"gunzip -c '/backups/$name' > /tmp/check.sqlite && sqlite3 -batch -noheader -list /tmp/check.sqlite 'PRAGMA integrity_check'")"
|
||||
echo "$name: integrity_check $result"
|
||||
[[ "$result" == "ok" ]]
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the development images on this Mac (native linux/arm64, nothing pushed):
|
||||
# kvitt-server:dev from server/Dockerfile (milestone M2a)
|
||||
# kvitt-backup:dev from infra/backup/Dockerfile
|
||||
# Usage: infra/dev/build.sh [server|backup|all] (default: all)
|
||||
# Refuses to run while a Gradle build holds the gradle-serial lock (CLAUDE.md: never build images during a Gradle build).
|
||||
set -euo pipefail
|
||||
# shellcheck source=lib.sh
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
what="${1:-all}"
|
||||
lock=/tmp/kvitt-gradle.lock
|
||||
if [[ -d "$lock" ]] && pid="$(cat "$lock/pid" 2>/dev/null)" && [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
echo "build.sh: a Gradle build is running (pid $pid, $lock); try again when it is done." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
server_image="$(env_value KVITT_SERVER_IMAGE)"; server_image="${server_image:-kvitt-server:dev}"
|
||||
backup_image="$(env_value KVITT_BACKUP_IMAGE)"; backup_image="${backup_image:-kvitt-backup:dev}"
|
||||
|
||||
build_server() {
|
||||
local dockerfile="$repo_root/server/Dockerfile"
|
||||
local context="${KVITT_SERVER_BUILD_CONTEXT:-$repo_root/server}"
|
||||
if [[ ! -f "$dockerfile" ]]; then
|
||||
echo "build.sh: $dockerfile does not exist yet (it comes with milestone M2a)." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Building $server_image from server/Dockerfile"
|
||||
docker build --platform linux/arm64 -f "$dockerfile" -t "$server_image" "$context"
|
||||
}
|
||||
|
||||
build_backup() {
|
||||
echo "Building $backup_image from infra/backup/Dockerfile"
|
||||
docker build --platform linux/arm64 -f "$repo_root/infra/backup/Dockerfile" -t "$backup_image" "$repo_root/infra/backup"
|
||||
}
|
||||
|
||||
case "$what" in
|
||||
server) build_server ;;
|
||||
backup) build_backup ;;
|
||||
all) build_backup; build_server ;;
|
||||
*) echo "usage: $0 [server|backup|all]" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,86 @@
|
||||
# Kvitt development stack on the owner's Mac (SPEC §10 "Development first", D-001).
|
||||
# Not the Pi stack: that is infra/compose.yaml, built by M2c-pi.
|
||||
#
|
||||
# No Caddy and no TLS here: debug builds accept http:// to private LAN addresses (AC-2.8), so the
|
||||
# server publishes its own port on the Mac. Release builds never talk to this stack.
|
||||
#
|
||||
# Use the scripts next to this file (see infra/README.md):
|
||||
# infra/dev/build.sh build kvitt-server:dev (from server/Dockerfile) and kvitt-backup:dev
|
||||
# infra/dev/up.sh start the server, wait for /health, print the LAN URL
|
||||
# infra/dev/logs.sh follow the logs
|
||||
# infra/dev/backup.sh run the backup job once (local snapshot, no Drive upload)
|
||||
# Relative paths below resolve against this directory (infra/dev/).
|
||||
|
||||
name: kvitt-dev
|
||||
|
||||
x-logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
services:
|
||||
server:
|
||||
image: ${KVITT_SERVER_IMAGE:-kvitt-server:dev}
|
||||
command: ["serve"]
|
||||
restart: unless-stopped
|
||||
user: "10001:10001"
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 160m
|
||||
ports:
|
||||
# The only published port of the dev stack; reachable from the phone on the home Wi-Fi.
|
||||
- "${KVITT_DEV_PORT:-8080}:8080"
|
||||
environment:
|
||||
TZ: Europe/Stockholm
|
||||
KVITT_DATA_DIR: /data
|
||||
KVITT_BIND: 0.0.0.0:8080
|
||||
# Invite links and the landing page point here; up.sh fills it from the Mac's LAN address when empty.
|
||||
KVITT_PUBLIC_URL: ${KVITT_PUBLIC_URL:-http://localhost:8080}
|
||||
KVITT_FCM_CREDENTIALS: /secrets/fcm-service-account.json
|
||||
KVITT_FCM_PROJECT_ID: ${KVITT_FCM_PROJECT_ID:-kvitt-1337}
|
||||
RUST_LOG: ${RUST_LOG:-info}
|
||||
volumes:
|
||||
# SQLite database, pictures and APKs: a bind-mounted folder on the Mac (git-ignored).
|
||||
- ${KVITT_DEV_DATA_DIR:-./data}:/data
|
||||
# The secrets folder, read-only, so the server finds /secrets/fcm-service-account.json (git-ignored).
|
||||
# The folder is mounted rather than the file: a missing source file would make Docker create a directory
|
||||
# in its place, while a missing key must simply leave push disabled with a start-up warning (§10.4).
|
||||
- ${KVITT_SECRETS_DIR:-../secrets}:/secrets:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "kvitt-server", "healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
logging: *logging
|
||||
networks: [kvitt]
|
||||
|
||||
backup:
|
||||
image: ${KVITT_BACKUP_IMAGE:-kvitt-backup:dev}
|
||||
# On demand only: `docker compose run --rm backup /backup.sh` (infra/dev/backup.sh). `up` skips it.
|
||||
profiles: [backup]
|
||||
command: ["/backup.sh"]
|
||||
restart: "no"
|
||||
# Same uid as the server, so it can open the WAL database files the server owns.
|
||||
user: "10001:10001"
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 96m
|
||||
environment:
|
||||
TZ: Europe/Stockholm
|
||||
# Empty on the dev stack: the Drive upload is skipped and the local files are kept (§10.9, D-001).
|
||||
RCLONE_FOLDER_ID: ""
|
||||
RCLONE_REMOTE: ${RCLONE_REMOTE:-gdrive:}
|
||||
volumes:
|
||||
- ${KVITT_DEV_DATA_DIR:-./data}:/data
|
||||
- ${KVITT_DEV_BACKUP_DIR:-./backups}:/backups
|
||||
logging: *logging
|
||||
networks: [kvitt]
|
||||
|
||||
networks:
|
||||
kvitt:
|
||||
driver: bridge
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copies the Firebase config (both Android clients, se.hagstedt.kvitt and se.hagstedt.kvitt.debug) from
|
||||
# infra/secrets/google-services.json to android/app/google-services.json in this checkout. Both are git-ignored.
|
||||
# Usage: infra/dev/copy-google-services.sh [source checkout] (default: this checkout; in a worktree, pass the main checkout)
|
||||
set -euo pipefail
|
||||
# shellcheck source=lib.sh
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
src_root="${1:-$repo_root}"
|
||||
src="$src_root/infra/secrets/google-services.json"
|
||||
dst="$repo_root/android/app/google-services.json"
|
||||
|
||||
[[ -f "$src" ]] || { echo "copy-google-services.sh: $src not found (created by the M2c-dev Firebase setup)." >&2; exit 1; }
|
||||
git -C "$repo_root" check-ignore -q "android/app/google-services.json" || {
|
||||
echo "copy-google-services.sh: android/app/google-services.json is not git-ignored; refusing to copy." >&2
|
||||
exit 1
|
||||
}
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp "$src" "$dst"
|
||||
chmod 600 "$dst"
|
||||
echo "Copied google-services.json to $dst"
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for the infra/dev scripts. Sourced, not run.
|
||||
# shellcheck disable=SC2034 # variables are used by the scripts that source this file
|
||||
|
||||
dev_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$dev_dir/../.." && pwd)"
|
||||
|
||||
# infra/dev/.env (git-ignored) when it exists, otherwise the committed example.
|
||||
if [[ -f "$dev_dir/.env" ]]; then env_file="$dev_dir/.env"; else env_file="$dev_dir/.env.example"; fi
|
||||
|
||||
# Value of KEY in the env file (empty when unset or blank).
|
||||
env_value() {
|
||||
sed -n "s/^$1=//p" "$env_file" | tail -n 1
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "$dev_dir/compose.yaml" --env-file "$env_file" "$@"
|
||||
}
|
||||
|
||||
# Resolve a path from the env file the way compose does: relative to infra/dev/.
|
||||
dev_path() {
|
||||
case "$1" in
|
||||
/*) printf '%s\n' "$1" ;;
|
||||
*) printf '%s\n' "$dev_dir/$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The Mac's address on the interface that carries the default route (the home Wi-Fi).
|
||||
lan_address() {
|
||||
local iface
|
||||
iface="$(route -n get default 2>/dev/null | awk '/interface:/ {print $2}')"
|
||||
[[ -n "$iface" ]] && ipconfig getifaddr "$iface" 2>/dev/null
|
||||
}
|
||||
|
||||
dev_port() {
|
||||
local p
|
||||
p="$(env_value KVITT_DEV_PORT)"
|
||||
printf '%s\n' "${p:-8080}"
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Follows the development stack's logs. Extra arguments go to `docker compose logs`
|
||||
# (for example: infra/dev/logs.sh --tail 100 server).
|
||||
set -euo pipefail
|
||||
# shellcheck source=lib.sh
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
compose logs -f "$@"
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Starts the development server (infra/dev/compose.yaml), waits for GET /health and prints the URLs.
|
||||
# KVITT_PUBLIC_URL defaults to http://<Mac LAN address>:<port> when the env file leaves it empty.
|
||||
# Usage: infra/dev/up.sh Stop with: infra/dev/up.sh --down
|
||||
set -euo pipefail
|
||||
# shellcheck source=lib.sh
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
if [[ "${1:-}" == "--down" ]]; then
|
||||
compose down
|
||||
exit 0
|
||||
fi
|
||||
|
||||
port="$(dev_port)"
|
||||
lan="$(lan_address || true)"
|
||||
|
||||
public_url="$(env_value KVITT_PUBLIC_URL)"
|
||||
if [[ -z "$public_url" ]]; then
|
||||
if [[ -n "$lan" ]]; then public_url="http://$lan:$port"; else public_url="http://localhost:$port"; fi
|
||||
fi
|
||||
export KVITT_PUBLIC_URL="$public_url"
|
||||
|
||||
data_dir="$(dev_path "$(env_value KVITT_DEV_DATA_DIR)")"
|
||||
secrets_dir="$(dev_path "$(env_value KVITT_SECRETS_DIR)")"
|
||||
mkdir -p "$data_dir"
|
||||
if [[ ! -f "$secrets_dir/fcm-service-account.json" ]]; then
|
||||
echo "up.sh: no $secrets_dir/fcm-service-account.json; the server starts with push disabled." >&2
|
||||
fi
|
||||
|
||||
compose up -d server
|
||||
|
||||
echo "Waiting for http://localhost:$port/health ..."
|
||||
for _ in $(seq 1 30); do
|
||||
if body="$(curl -fsS "http://localhost:$port/health" 2>/dev/null)"; then
|
||||
echo "health: $body"
|
||||
echo "Mac: http://localhost:$port"
|
||||
[[ -n "$lan" ]] && echo "Phone: http://$lan:$port (home Wi-Fi)"
|
||||
echo "Invites: $public_url"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "up.sh: /health did not answer within 30 s; see infra/dev/logs.sh" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user