Check in implementable deploy/docker-compose.yml, .env.example, and Caddy fragment. Store URL is env SoT (not a settings row). Application Password setup stays in DEPLOYMENT §5 (issue #5).
9.1 KiB
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):
docker ps— existing containers, published ports (avoid collisions with co-resident apps; bolt.diy is one known example of 8443-class port use).- Caddy config location (host file or container) and existing site blocks.
- Free port choice for the compose stack (proposal:
18080http,18443https — verify free on this host). - Docker volume dir convention on the host.
- 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_*, WOO_BASE_URL (https only)
│ └── 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 (checked in)
Canonical file: deploy/docker-compose.yml. Placeholders: deploy/.env.example.
From repo root:
cp deploy/.env.example deploy/.env && chmod 600 deploy/.env
# fill SLAB_AES_KEY, APP_HOSTNAME, WOO_BASE_URL (https:// only)
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d
Caddy site block: deploy/Caddyfile.fragment (merge into host Caddy). Compose
service names frontend and fastapi must match that fragment.
WOO_BASE_URL (issue #11 — plan of record):
- Set in
deploy/.envand injected into the FastAPI container at start. - HTTPS only. FastAPI must refuse missing /
http://values at startup. - Not a Settings input and not a SQLite settings row.
- Settings UI shows the URL as read-only text (which store this deploy hits).
- Change store = edit
.env+ recreate the fastapi container.
Woo Application Password stays out of compose (AES-GCM via Settings; issue #5).
Conventions (house rules):
mem_limiton services (not host-level tuning).- Services bind to 127.0.0.1 only; Caddy is the only public surface.
- No
latesttags on base images — pin versions. restart: unless-stopped; compose is the supervisor (no systemd units).
4. Caddy (fragment)
Canonical file: deploy/Caddyfile.fragment. API must win over the frontend
catch-all. Put handle /api/* before the frontend handle block:
{$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 |
deploy/.env (mode 600) → compose env |
32-byte base64; NEVER in the repo |
WOO_BASE_URL |
deploy/.env → compose → FastAPI env |
https:// only; Settings read-only |
| Woo WP username + application password | SQLite settings (AES-GCM) via Settings UI |
dedicated low-priv WP user; not consumer keys |
| Inference API key | SQLite settings (AES-GCM) via Settings UI |
usually empty for local llama.cpp |
.env.example |
repo | placeholders only, committed |
Woo credential setup (once per store):
- Store must already be on HTTPS. WordPress will not offer Application Passwords over plain HTTP.
- Create a dedicated low-privilege WordPress user (shop manager, or custom role with product create/edit + media upload). Do not reuse an admin login.
- Sign in as that user → Users → Profile → Application Passwords → generate a password for "SlabUploader".
- Enter the username + application password in the app Settings UI. FastAPI stores them AES-GCM encrypted. Browser never keeps them.
- Rotate by revoking the old application password in the WP profile and
re-entering a new one in Settings, then
test-woo.
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 | WP user profile → revoke old application password → new one → Settings UI → POST /api/v1/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)
GET https://${APP_HOSTNAME}/api/health→ ok (from phone over HTTPS once CA trust is proven)- Settings: WP application password +
test-woook;woo_create_status=draft; inference OFF for early gates - Create a slab → upload test photos (fixture set) → poll to
ready - Verify bdft/price match the fixture's expected values (golden test; bdft = sqft × thickness)
- Publish test slab with SKU
SLAB-UAT-…→ slab may show localpublished; Woo product must be draft - 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_minbelow 1600; undersized source means retake, not a smaller min. RemBG is not part of the hybrid happy path.