From 2d9cae262434bcf9f80808009873573485e81a0b Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 09:58:52 -0400 Subject: [PATCH 1/2] =?UTF-8?q?deploy:=20P4=20stack=20kit=20=E2=80=94=20do?= =?UTF-8?q?cker-compose=20(app=20+=20Tailscale=20sidecar,=20userspace),=20?= =?UTF-8?q?systemd=20unit=20(Delegate=3Dyes),=20Dockerfile=20on=203.14-sli?= =?UTF-8?q?m=20with=20--state-dir,=20serve.json,=20gitignore=20stack-local?= =?UTF-8?q?=20.env/state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++ deploy/Dockerfile | 15 +++--- deploy/docker-compose.yml | 64 +++++++++++++++++++++++++ deploy/podman-stack-bike-tinder.service | 18 +++++++ deploy/tailscale/serve.json | 10 ++++ 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/podman-stack-bike-tinder.service create mode 100644 deploy/tailscale/serve.json diff --git a/.gitignore b/.gitignore index 306fe8b..a9ff7c3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ data/ *.sqlite3-wal *.sqlite3-shm +# deploy state (stack-local, never committed) +.env +tailscale-state/ + # python __pycache__/ *.pyc diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 2e3daec..709ec1c 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,15 +1,18 @@ -# Bike-Tinder app container (Phase 2 build). -# The compose stack, systemd unit and Tailscale sidecar belong to the parent — +# Bike-Tinder app container (Phase 4 deploy). +# The compose stack, systemd unit and Tailscale sidecar live beside this file — # this file is only the app image contract. -FROM python:3.13-slim +FROM python:3.14-slim WORKDIR /app -COPY requirements.txt . +COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY app/ ./app/ -# Data lands on the stack volume: /data/bike-tinder.sqlite3 and /data/img/ +# Data lands on the stack volume: /data/bike-tinder.sqlite3 and /data/img/. +# /state is the hunt state dir (bind-mounted read-only) used by POST /api/import. EXPOSE 8000 -CMD ["python", "-m", "app.serve", "--host", "0.0.0.0", "--port", "8000", "--db", "/data/bike-tinder.sqlite3", "--img-dir", "/data/img"] +CMD ["python", "-m", "app.serve", "--host", "0.0.0.0", "--port", "8000", \ + "--db", "/data/bike-tinder.sqlite3", "--img-dir", "/data/img", \ + "--state-dir", "/state"] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..b9c0ff0 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,64 @@ +# Bike-Tinder stack — deck app + Tailscale sidecar. +# +# Deployed on Rotarran at /var/mnt/DATA/podman-stacks/bike-tinder/ (a checkout +# of the `release` branch). Run via the user unit podman-stack-bike-tinder.service +# (Delegate=yes) or manually: +# podman-compose -f deploy/docker-compose.yml --env-file .env up -d +# +# Networking: +# - app: published on 127.0.0.1:9453 only — Hermes cron triggers (import / +# agent read) and local debugging. The LAN sees nothing. +# - tailscale (userspace mode, no tun/caps): joins the tailnet as `biketinder` +# and serves https://biketinder.tailf35a34.ts.net -> http://app:8000 (443). +# Secrets: TS_AUTHKEY + OPENROUTER_API_KEY come from the stack-local .env +# (never committed). Before the authkey is placed, the sidecar simply reports +# "no authkey" and exits — the app runs fine on its own. + +x-logging: &default-logging + driver: "json-file" + options: + max-size: "50m" + max-file: "5" + compress: "true" + +services: + app: + build: + context: .. + dockerfile: deploy/Dockerfile + image: localhost/bike-tinder:latest + container_name: bike-tinder-app + restart: unless-stopped + environment: + - TZ=America/Halifax + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} + volumes: + - /var/mnt/DATA/podman-stacks/bike-tinder/data:/data + - /var/home/admin/.hermes/scripts/motorcycle-hunt.d:/state:ro,z + ports: + - "127.0.0.1:9453:8000" + stop_grace_period: 15s + security_opt: + - no-new-privileges:true + logging: + <<: *default-logging + + tailscale: + image: docker.io/tailscale/tailscale:latest + container_name: bike-tinder-ts + hostname: biketinder + restart: unless-stopped + depends_on: + - app + environment: + - TS_AUTHKEY=${TS_AUTHKEY:-} + - TS_HOSTNAME=biketinder + - TS_STATE_DIR=/var/lib/tailscale + - TS_SERVE_CONFIG=/config/serve.json + - TS_USERSPACE=true + - TZ=America/Halifax + volumes: + - /var/mnt/DATA/podman-stacks/bike-tinder/tailscale-state:/var/lib/tailscale + - /var/mnt/DATA/podman-stacks/bike-tinder/deploy/tailscale:/config:ro + logging: + <<: *default-logging diff --git a/deploy/podman-stack-bike-tinder.service b/deploy/podman-stack-bike-tinder.service new file mode 100644 index 0000000..caab057 --- /dev/null +++ b/deploy/podman-stack-bike-tinder.service @@ -0,0 +1,18 @@ +# /var/home/admin/.config/systemd/user/podman-stack-bike-tinder.service +# (installed from the repo: deploy/podman-stack-bike-tinder.service) +[Unit] +Description=Bike-Tinder podman-compose stack (deck app + Tailscale sidecar) +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +Delegate=yes +WorkingDirectory=/var/mnt/DATA/podman-stacks/bike-tinder +ExecStart=/var/home/admin/.local/bin/podman-compose -f deploy/docker-compose.yml --env-file .env up -d +ExecStop=/var/home/admin/.local/bin/podman-compose -f deploy/docker-compose.yml --env-file .env down +TimeoutStartSec=600 + +[Install] +WantedBy=default.target diff --git a/deploy/tailscale/serve.json b/deploy/tailscale/serve.json new file mode 100644 index 0000000..0e78497 --- /dev/null +++ b/deploy/tailscale/serve.json @@ -0,0 +1,10 @@ +{ + "TCP": { "443": { "HTTPS": true } }, + "Web": { + "${TS_CERT_DOMAIN}:443": { + "Handlers": { + "/": { "Proxy": "http://app:8000" } + } + } + } +} From d663f1eeb5603c7f35e654311331b3c45ceca5d7 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 09:59:45 -0400 Subject: [PATCH 2/2] =?UTF-8?q?deploy:=20P4=20runbook=20=E2=80=94=20deploy?= =?UTF-8?q?/rollback/logs/import=20gating/backup-restore=20drill/phone=20c?= =?UTF-8?q?hecklist=20+=20one-time=20sidecar=20key=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DESIGN.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 6ec034a..752bb74 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -116,6 +116,29 @@ data/ runtime state (gitignored) docs/ this design set ``` -## Runbook (completed at Phase 4) +## Runbook (Phase 4) -Deploy / update / rollback steps, log locations, import troubleshooting (gates), backup-restore drill, phone verification checklist. +**Stack home:** `/var/mnt/DATA/podman-stacks/bike-tinder/` — a checkout of the `release` branch, plus stack-local `data/` (DB + image mirror + backups), `.env` (secrets, never committed) and `tailscale-state/` (node identity). Unit: `podman-stack-bike-tinder.service` (installed from `deploy/`). + +**Deploy / update a cut:** + +```sh +# from the repo: merge dev -> main, then fast-forward release +git -C /var/mnt/DATA/podman-stacks/bike-tinder pull --ff-only +podman-compose -f deploy/docker-compose.yml --env-file .env build +systemctl --user restart podman-stack-bike-tinder.service +``` + +Docs-only cuts stop after the `pull`. The unit is `oneshot` + `RemainAfterExit` with `Delegate=yes`, so the stack survives `hermes gateway restart` and reboots. The historical dev service (`bike-tinder-dev.service`) is retired at deploy; rollback happens through the stack, not that unit. + +**Rollback:** check out the previous release SHA in the stack dir, rebuild, restart. The DB is never touched by code rollbacks (migrations are guarded and idempotent); if a migration ever needs unwinding, restore the last backup (below). + +**Logs:** `podman logs bike-tinder-app`, `podman logs bike-tinder-ts`, `journalctl --user -u podman-stack-bike-tinder.service`. + +**Import troubleshooting:** `POST /api/import` refuses when the hunt state is older than 26 h or missing — check `~/.hermes/scripts/motorcycle-hunt.d/*.json`. The nightly chain is the Hermes cron job `bike-tinder-import` (08:30 Atlantic: import → agent read → DB backup); it is silent on success by design — inspect its last run in the cron list if a morning looks stale. The container also runs an on-boot catch-up import when the last run is >26 h old. + +**Backups:** nightly `data/backup/bike-tinder-YYYYMMDD-HHMM.sqlite3`, last 14 kept. Restore drill: stop the unit → remove `data/bike-tinder.sqlite3-wal`/`-shm` → copy the chosen backup over `data/bike-tinder.sqlite3` → start the unit → check `GET /api/stats`. + +**Phone checklist:** open `https://biketinder.tailf35a34.ts.net` from a tailnet device → expect the deck; Add to Home Screen (PWA installs via the manifest); swipe once and confirm the decision shows in List. If the URL fails: check the sidecar (`podman logs bike-tinder-ts`), confirm `TS_AUTHKEY` in `.env`, and re-run `podman-compose -f deploy/docker-compose.yml --env-file .env up -d`. + +**First-run sidecar (one-time):** mint a Tailscale auth key (admin console → Settings → Keys), put `TS_AUTHKEY=` into the stack `.env`, then `podman-compose -f deploy/docker-compose.yml --env-file .env up -d tailscale`.