Files
SlabUploader/docs/DEPLOYMENT.md
T
grokbot_ty 369d404c29 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.
2026-09-01 05:26:45 +00:00

181 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Deployment Playbook (Docker Compose + Caddy)
Status: SPEC · env-agnostic · 2026-09-01 · SlabUploader
**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`.
> 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, 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 on **this** host).
4. Docker volume dir convention on the host.
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)
```
SlabUploader/
├── AGENTS.md
├── README.md
├── docs/ # this plan set
├── deploy/
│ ├── docker-compose.yml
│ ├── Caddyfile.fragment # slab site block (merged into host Caddy)
│ ├── 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
└── 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
│ ├── woo/ # Woo client, taxonomy, publish
│ ├── inference/ # vision + content wrappers (toggleable)
│ ├── prompts/ # call1.txt, call2.txt (shipped defaults)
│ ├── api/ # routers (slabs, photos, pricing, taxonomy, settings)
│ ├── db/ # schema.sql, models, migrations (alembic)
│ └── security/ # AES-GCM settings encryption
├── tests/ # pytest — unit (deterministic) + integration (woo)
└── Dockerfile
```
## 3. docker-compose.yml (target shape)
```yaml
services:
frontend:
build: ./frontend
ports: ["127.0.0.1:${FRONTEND_PORT:-18080}:80"] # Caddy-only; not public
depends_on: [fastapi]
restart: unless-stopped
fastapi:
build: ./backend
environment:
SLAB_AES_KEY: ${SLAB_AES_KEY} # from .env (32-byte base64)
# no Woo/inference keys here — they live encrypted in SQLite (Settings UI)
volumes:
- slab_db:/data
- slab_images:/images
- ./prompts:/app/prompts:ro # prompt files, read-only mount
ports: ["127.0.0.1:${FASTAPI_PORT:-18081}:8000"] # Caddy-only
mem_limit: 2g # env convention: mem_limit preferred
restart: unless-stopped
volumes:
slab_db:
slab_images:
```
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.
- `restart: unless-stopped`; compose is the supervisor (no systemd units).
## 4. Caddy (fragment)
API must win over the frontend catch-all. Put `handle /api/*` **before** the frontend
`handle` block:
```caddyfile
{$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 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
| Secret | Where | Notes |
|---|---|---|
| `SLAB_AES_KEY` | `.env` on host (mode 600) → compose env | 32-byte base64; NEVER in the repo |
| Woo consumer key/secret | SQLite `settings` (AES-GCM) via Settings UI | rotate in Woo → re-enter in UI |
| Inference API key | SQLite `settings` (AES-GCM) via Settings UI | usually empty for local llama.cpp |
| `.env.example` | repo | placeholders only, committed |
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 and restore
`deploy/backup.sh` (run via cron on the app host, 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 .
3. cp -r /backups/prompts-YYYYMMDD.tar.gz prompts/ # prompt files
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 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.
## 7. Operations runbook
| Task | Command / how |
|---|---|
| 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://${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) |
| Edit a prompt | Edit the text file on host, no restart needed (read fresh each time) |
| Rollback image | `git checkout <tag>` → rebuild → up -d; DB down-migrations provided for schema changes |
| Reset one slab | delete its photos dir + `UPDATE slabs SET status='draft'` (admin script) |
## 8. Smoke test (after every deploy)
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)
5. Publish test slab with SKU `SLAB-UAT-…` → slab may show local `published`; Woo product must be **draft**
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 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.