docs: env-agnostic deploy (APP_HOSTNAME, inventory, Caddy order)

DEPLOYMENT is host-variable: APP_HOST/APP_HOSTNAME/ports, inventory
worksheet, corrected Caddy handle /api/* before frontend. Drop RemBG
and output_px 1000 as deploy SoT. AGENTS §9, README, IMPL host wording
match. Historical .201 lab is a non-normative callout only.
This commit is contained in:
2026-09-01 05:26:45 +00:00
parent 48720d83e1
commit 369d404c29
5 changed files with 133 additions and 47 deletions
+56 -38
View File
@@ -1,29 +1,34 @@
# Deployment Playbook (.201, Docker Compose + Caddy)
# Deployment Playbook (Docker Compose + Caddy)
Status: SPEC (deliverable #4) · 2026-08-24 · SlabUploader
Target: TyUBUMini at 192.168.1.201. Domain: `slab.tyubumini.local` (LAN/Tailscale
Status: SPEC · env-agnostic · 2026-09-01 · SlabUploader
> Gate A freeze (2026-08-31): hybrid client owns measure/crop SoT. Do not scaffold a server ruler/pipeline as authoritative. Woo UAT = draft + SLAB-UAT-* on PROD.
only — Decision). Inference is NOT on .201; it's a remote configurable endpoint
(.202 or elsewhere).
**Target:** any inventoried Docker host. Fill `deploy/INVENTORY.md` before bind.
Variables: `APP_HOST`, `APP_HOSTNAME`, `FRONTEND_PORT`, `FASTAPI_PORT`.
Health URL: `https://${APP_HOSTNAME}/api/health`.
> Note: exact Caddyfile/ports to be verified against the live .201 Caddy setup at
> implementation time (bolt.diy already runs Caddy TLS on tyubumini.local:8443).
> This playbook is the plan; the first implementation task is a .201 inventory pass.
> Gate A freeze (2026-08-31): hybrid client owns measure/crop SoT. Do not scaffold a
> server ruler/pipeline as authoritative. Woo UAT = draft + `SLAB-UAT-*` on PROD.
> Inference is **not** on the app host by default. It is a remote configurable endpoint.
**Historical lab example (non-normative):** TyUBUMini at `192.168.1.201` with hostname
`slab.tyubumini.local` was one early candidate. Do not treat that IP or name as SoT.
Use the inventory worksheet.
## 1. Host inventory (do first — do not assume)
Before deploying, record the current state (HARD RULE: inventory before execute):
1. `docker ps` — existing containers, published ports (avoid collisions; bolt.diy
owns 8443/8444-class ports).
2. Caddy config location (host file or container) and existing `tyubumini.local`
site blocks.
Before deploying, fill `deploy/INVENTORY.md` (hard rule: inventory before execute):
1. `docker ps` — existing containers, published ports (avoid collisions with
co-resident apps; bolt.diy is one known example of 8443-class port use).
2. Caddy config location (host file or container) and existing site blocks.
3. Free port choice for the compose stack (proposal: `18080` http, `18443` https —
verify free).
verify free on **this** host).
4. Docker volume dir convention on the host.
5. Tailscale: confirm `slab.tyubumini.local` is reachable from the phone's network
(same LAN or Tailscale). If the domain isn't in DNS yet, add it (Pi-hole/AdGuard
or Tailscale MagicDNS) — one record, A → 192.168.1.201 (LAN) + Tailscale name.
5. Reachability: confirm `${APP_HOSTNAME}` is reachable from the phone network
(same LAN or Tailscale). Add DNS (Pi-hole/AdGuard or Tailscale MagicDNS) if needed:
one A record → `${APP_HOST}` (LAN and/or Tailscale name).
Write the chosen values into `deploy/INVENTORY.md` and the host `.env` before compose up.
## 2. Repository layout (at scaffold)
@@ -35,10 +40,11 @@ SlabUploader/
├── deploy/
│ ├── docker-compose.yml
│ ├── Caddyfile.fragment # slab site block (merged into host Caddy)
│ ├── .env.example # SLAB_AES_KEY, port vars (no real secrets)
│ ├── INVENTORY.md # filled at Gate B on the designated host
│ ├── .env.example # SLAB_AES_KEY, APP_*, port vars (no real secrets)
│ └── backup.sh # sqlite .backup + images tar, with retention
├── frontend/ # SvelteKit app (Phase 1)
└── backend/ # FastAPI app (Phase 0)
├── frontend/ # SvelteKit app
└── backend/ # FastAPI app
├── app/
│ ├── main.py
│ ├── (no happy-path pipeline SoT here) # hybrid: TECH-SPEC modules live in frontend/ or shared pure TS; backend = store/Woo/inference/U2Net stub
@@ -81,6 +87,7 @@ volumes:
```
Conventions (house rules):
- `mem_limit` on services (not host-level tuning).
- Services bind to 127.0.0.1 only; Caddy is the only public surface.
- No `latest` tags on base images — pin versions.
@@ -88,21 +95,27 @@ Conventions (house rules):
## 4. Caddy (fragment)
API must win over the frontend catch-all. Put `handle /api/*` **before** the frontend
`handle` block:
```caddyfile
slab.tyubumini.local {
tls internal # Caddy internal CA (matches bolt.diy pattern)
reverse_proxy frontend:80 {
header_up X-Real-IP {remote_host}
}
{$APP_HOSTNAME} {
tls internal # Caddy internal CA (or host-appropriate TLS)
handle /api/* {
reverse_proxy fastapi:8000
}
handle {
reverse_proxy frontend:80 {
header_up X-Real-IP {remote_host}
}
}
}
```
- HTTPS enforced (http→https redirect automatic).
- If the host Caddy already serves `*.tyubumini.local`, add this as a site block;
verify the internal CA cert is trusted on the phone (same flow as bolt.diy).
- LAN/Tailscale only — no public exposure.
- HTTPS enforced (http→https redirect automatic when using Caddy TLS).
- If the host Caddy already serves a wildcard for this domain family, add this as a
site block. Verify the cert is trusted on the phone.
- LAN/Tailscale only — no public exposure for POC.
## 5. Secrets
@@ -117,9 +130,10 @@ Key rotation (AES): `SLAB_AES_KEY` change requires re-encrypting settings —
provide `python -m app.security.rekey NEW_KEY` (reads old key from env
`SLAB_AES_KEY_OLD`, decrypts, re-encrypts, updates). Documented in playbook §7.
## 6. Backup & restore
## 6. Backup and restore
`deploy/backup.sh` (run via cron on the app host, e.g. daily 03:00):
`deploy/backup.sh` (run via cron on .201, e.g. daily 03:00):
```
1. sqlite3 /data/slab.db ".backup /backups/slab-YYYYMMDD.db" # WAL-safe
2. tar czf /backups/slab-images-YYYYMMDD.tgz -C /images .
@@ -127,8 +141,9 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
4. retain: 7 daily + 4 weekly; prune older
5. log result to /var/log/slab-backup.log; alert (Hermes) on failure
```
- Backup target: local dir first; mirror to TrueNAS SMB share
(`\\truenas.local\…\backups\slab`) for off-box safety (same pattern as other stacks).
- Backup target: local dir first; mirror to an off-box share for safety (same pattern
as other stacks on the host).
- **Restore**: stop compose → restore DB file + untar images → start compose.
Tested at least once during Phase 6 (UAT) before declaring backup done.
@@ -139,7 +154,7 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
| Deploy (first) | inventory §1 → `docker compose -f deploy/docker-compose.yml build` → `up -d` → Caddy reload → smoke §8 |
| Update | `git pull` → `docker compose build fastapi frontend` → `up -d` (DB migrations run on start via alembic) |
| Logs | `docker compose logs -f fastapi` / `frontend` |
| Health | `curl -k https://slab.tyubumini.local/api/health` |
| Health | `curl -k https://${APP_HOSTNAME}/api/health` |
| Rekey AES | §5 |
| Rotate Woo creds | Woo UI → new consumer key/secret → Settings UI → `POST /api/settings/test-woo` |
| Rotate inference key | Settings UI (if external) |
@@ -148,7 +163,8 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
| Reset one slab | delete its photos dir + `UPDATE slabs SET status='draft'` (admin script) |
## 8. Smoke test (after every deploy)
1. `GET /api/health` → ok (from phone over HTTPS once CA trust is proven)
1. `GET https://${APP_HOSTNAME}/api/health` → ok (from phone over HTTPS once CA trust is proven)
2. Settings: Woo creds + `test-woo` ok; `woo_create_status=draft`; inference OFF for early gates
3. Create a slab → upload test photos (fixture set) → poll to `ready`
4. Verify bdft/price match the fixture's expected values (golden test; bdft = sqft × thickness)
@@ -156,7 +172,9 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
6. Leave or delete the Woo draft UAT product (never customer-visible). Final live publish is a non-UAT SKU after Ty review.
## 9. Scaling path (not v1)
- Postgres when SQLite write contention appears (schema already portable).
- Redis+arq when in-process queue shows lag.
- GPU on .201 only if RemBG latency becomes a bottleneck — otherwise keep it CPU
(normalization budget is 3–5s per PRD; U²Net on modern CPU meets that).
- GPU on the app host only if on-demand U2Net latency becomes a bottleneck. Keep CPU
first. Never lower `output_px_min` below 1600; undersized source means retake, not a
smaller min. RemBG is not part of the hybrid happy path.