# DEPLOY-FAST — minutes, not seven (2026-09-24, agent P8)

James's §6.5 wish. Nothing here was run against Docker (none on the Mac); the
Dockerfile logic is verified by reading, the Next config by two local builds,
the scripts by `bash -n`. The first server deploy is the real test.

## What changed and why

| Change | File | Effect |
|---|---|---|
| Build context excludes `docs`, screenshots, `.data`, tsbuildinfo, logs, tests, handover archives | `.dockerignore` | context ~966 MB → ~30 MB (437 MB was `apps/paiduay/docs/screenshots`); no build step reads any `docs/` (grep: zero imports) |
| Manifests-first layering: lockfile + every workspace `package.json` → `pnpm install` → then source | `deploy/Dockerfile` (`manifests`, `deps`, `build` stages) | the install layer is cached until a dependency changes; a source edit no longer re-resolves it (~1 min saved) |
| pnpm store in a BuildKit cache mount (`id=pnpm-store`) | `deploy/Dockerfile` | a dependency bump downloads only the new packages |
| In-image type check skipped (`NEXT_SKIP_TYPECHECK=1`, paiduay only) | `deploy/Dockerfile`, `deploy/compose.yaml`, `apps/paiduay/next.config.ts` | ~1 min saved; root `pnpm typecheck` gates every commit. Local builds are unchanged (the flag is only set by compose) |
| Next standalone runtime, GATED OFF (`STANDALONE=1` build arg / `PAIDUAY_STANDALONE=1`) | same three files | when proven: runtime image ~200 MB instead of 3.66 GB, so image export/unpack (~2 min today) collapses |
| One detached deploy command | `deploy/deploy.sh` (server), `deploy/push-and-deploy.sh` (Mac) | ssh drops cannot kill a build; log under `/tmp`; waits for `https://frenday.xyz/story` → 200; prints elapsed; `NOTIFY=1` posts to James's n8n webhook |

## Expected timings (estimates, not measured)

| Deploy | Today | Now (STANDALONE=0) | With standalone |
|---|---|---|---|
| first build after this change | 7+ min | 6–8 min (cold install layer, empty store cache) | same |
| source-only change | 7+ min | ~3–4 min (context 1 s, install cached, `next build` ~2 min, export/unpack ~1–2 min) | ~2–3 min |
| dependency change | 7+ min | ~5 min | ~4 min |

## First deploy with the new pipeline

From the Mac, after the integrator commits:

```bash
deploy/push-and-deploy.sh              # paiduay; tails the server log until the verdict
NOTIFY=1 deploy/push-and-deploy.sh     # same, plus the n8n line
```

Or on the box:

```bash
cd ~/apps/ai-new-business/repo && git pull --ff-only && deploy/deploy.sh   # detached
tail -f /tmp/ai-factory-deploy-latest.log
deploy/deploy.sh --all                 # whole fleet + migrate (the old behaviour)
MIGRATE=1 deploy/deploy.sh paiduay     # rebuild ops + migrate first
```

The script only ever names ai-factory services (`demo clinic barber cameo
paiduay`), uses `up -d --no-deps`, never prunes, never touches Luna / TuaTon /
chatbot-engine.

## Standalone test recipe (throwaway container, never replaces `paiduay-1`)

```bash
cd ~/apps/ai-new-business/repo
DOCKER_BUILDKIT=1 docker build --network host -f deploy/Dockerfile \
  --secret id=envfile,src=deploy/.env \
  --build-arg APP=paiduay --build-arg PORT=3400 --build-arg STANDALONE=1 \
  -t paiduay-standalone-test .
docker run -d --name paiduay-standalone-test --env-file deploy/.env \
  -e PAIDUAY_UPLOAD_DIR=/data/uploads -e NEXT_PUBLIC_SITE_URL=https://frenday.xyz -e PUBLIC_ORIGIN_SUFFIX=frenday.xyz \
  -v ai-factory_paiduay-uploads:/data/uploads -p 127.0.0.1:3501:3400 paiduay-standalone-test
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3501/story          # 200
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:3501/c/mint/kit/image?t=band&f=x'   # fonts from assets/ -> 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3501/c/mint          # photo from the volume -> 200
docker logs paiduay-standalone-test | grep 'job worker started'                # instrumentation runs
docker rm -f paiduay-standalone-test; docker rmi paiduay-standalone-test
# flip production when it passes:
PAIDUAY_STANDALONE=1 deploy/deploy.sh paiduay
```

Known before you start: `lib/photo/store.ts:123` (a dynamic `stat`) makes the
tracer include the whole app dir (Turbopack warning). Harmless for correctness;
for size add `stat(/*turbopackIgnore: true*/ p)` there (app code, not P8's).
Standalone `server.js` ignores `start -p`: the port comes from the `PORT`
build arg. Never build `ops` standalone (needs tsx + scripts + migrations).

## Rollback

```bash
git checkout 9631f44 -- deploy/Dockerfile deploy/compose.yaml deploy/deploy.sh .dockerignore
git commit -m "revert: deploy pipeline to pre-P8"; deploy/push-and-deploy.sh
```
(`next.config.ts` can stay: its two knobs are inert without the env vars.)
Emergency: the previous image is still on the box as a dangling image
(`docker images -f dangling=true`); `docker tag <id> ai-factory-paiduay:latest
&& docker compose up -d --no-deps paiduay` restores it in seconds.

## End state: build OFF the VPS, the VPS only pulls

1. Builder: GitHub Actions (`docker/build-push-action`, cache `type=gha`) or
   the n8n VPS (James is root: install docker + buildx, an n8n workflow or a
   cron runs `docker build` on push).
2. Registry: GHCR (`ghcr.io/<owner>/ai-factory-paiduay`) or a self-hosted
   `registry:2` behind Caddy on the n8n VPS.
3. App VPS: compose `image:` instead of `build:`; deploy = `docker compose pull
   paiduay && up -d --no-deps paiduay` (~30 s, standalone image). Build-time
   prerender still needs Postgres: either an SSH tunnel from the builder to the
   VPS's Postgres, or make the prerendering pages `dynamic` so the build is
   DB-free (cleaner; audit which pages read tenant data at build).
4. Secrets: the builder needs the `.env` values that `next build` bakes
   (`NEXT_PUBLIC_*`, Clerk publishable key) as CI secrets, never the DB creds
   if step 3's DB-free build is done.

Questions for James: GitHub repo + GHCR token, or n8n VPS + which registry?
May the builder create a read-only pull token on the app VPS? May the 58 GB
builder cache on the app VPS be pruned (shared with the other projects)?
