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:
@@ -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