116 lines
8.0 KiB
Markdown
116 lines
8.0 KiB
Markdown
# 2026-05-03 — Production Prep
|
||
|
||
> Companion to `BuildLog.md`. Predecessor: `2026-05-03-reminders.md`.
|
||
|
||
## Milestone
|
||
|
||
A self-contained, reproducible production deployment story. The app + worker now build into a single Docker image (`touchbase/app:latest`, ~1.7 GB). Compose's `prod` profile brings up `postgres` + `app` + `worker` + `caddy` with sensible env defaults; the runbook in `docs/deploy.md` walks through first-time setup and ongoing operations.
|
||
|
||
## What landed
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `Dockerfile` | Multi-stage build (deps → builder → runner). Single image runs both `pnpm start` (web) and `pnpm worker` per command override. Non-root `nextjs` user, exposes 3000. |
|
||
| `.dockerignore` | Excludes node_modules, .next, .git, .env, IDE/OS junk, docs/progress (those ship with code but not in the build context to keep it lean) |
|
||
| `compose.yaml` | Updated `app` + `worker` to use `env_file: .env` with explicit `DATABASE_URL` override that points at the `postgres` service hostname (so prod profile works without manual env tweaking). Added Docker `HEALTHCHECK` on app via `/api/health`. |
|
||
| `src/app/api/health/route.ts` | Liveness/readiness endpoint. Returns `{status, version, time, checks: {app, db}}`. 200 if Postgres reachable; 503 otherwise. Cached 5s. |
|
||
| `next.config.ts` | Added `turbopack.root` to silence Next 16's "multiple lockfiles" warning at build time. |
|
||
| `src/lib/seed.ts` | Refuses to run in `NODE_ENV=production` unless `ALLOW_SEED_IN_PRODUCTION=1` is set. Reason: `seed()` `TRUNCATE`s every table; one accidental prod run wipes the database. |
|
||
| `docs/deploy.md` | First-time setup walkthrough, required env vars table, migrations / logs / backups / rollback / common ops sections, "what's not yet automated" section pointing at registry, secrets, observability, CI as next steps. |
|
||
|
||
## What's verified
|
||
|
||
- `pnpm test` — **92/92 green**
|
||
- `pnpm lint` — clean
|
||
- `pnpm exec tsc --noEmit` — clean
|
||
- `docker-compose --profile prod build app` — succeeds; produces `touchbase/app:latest` at 1.73 GB
|
||
- Live container smoke:
|
||
- Started `app` container against the dev Postgres → `/api/health` returns `{"status":"ok","checks":{"app":"ok","db":"ok"}}` and `/` returns 200 HTML
|
||
- Started `worker` container with `pnpm worker` command override → "[worker] pg-boss started; handlers registered, idling"
|
||
|
||
## Decisions ratified
|
||
|
||
| Decision | Resolution |
|
||
|---|---|
|
||
| Image strategy | **Single image, full deps** for both web and worker. Container picks via `command:` override. Reason: simpler than two images, ~1.7 GB is acceptable for v1. Optimization to standalone-mode + slim image deferred. |
|
||
| Base image | `node:22-bookworm-slim` (Debian) over `node:22-alpine`. Reason: better native-deps compatibility (sharp, esbuild) at the cost of ~80 MB. |
|
||
| pnpm in runner stage | `npm install --global pnpm@10.18.3` instead of `corepack prepare`. Reason: corepack writes to user-specific cache; the non-root `nextjs` user can't write to `~/.cache` if corepack-prepare ran as root in the image. Global npm install is simpler and works for any UID. |
|
||
| Healthcheck endpoint | `/api/health` with `db.$queryRaw\`SELECT 1\`` ping. 200 healthy / 503 degraded. Cached 5s so flapping monitors don't hammer the DB. |
|
||
| Compose env strategy | `env_file: .env` for convenience; explicit `DATABASE_URL: ${DATABASE_URL:-postgresql://...postgres:5432/...}` override so prod-profile-locally just works (uses internal docker network hostname). For prod deploy, user `export DATABASE_URL=...` to override before `docker-compose up`. |
|
||
| Seed safety in prod | `seed()` throws if `NODE_ENV=production` unless `ALLOW_SEED_IN_PRODUCTION=1`. Reason: forgetting that `seed` wipes data is a foot-gun; the env var makes "I really mean it" explicit. |
|
||
| Migration strategy | `pnpm exec prisma migrate deploy` from the host or via `docker-compose exec app`. Non-destructive; no resets. Documented in `docs/deploy.md`. |
|
||
| Where logs go | Container stdout/stderr → Docker's logging driver. Default json-file is fine for v1; structured logging (Pino) + remote sink is a "next step" item. |
|
||
| Image tag in compose | `touchbase/app:latest` (was `:dev`). Reason: clearer naming for prod. Real tag-by-SHA discipline waits until we have an image registry. |
|
||
|
||
## Gotchas hit
|
||
|
||
### corepack + non-root user
|
||
First boot of the app container crashed with `EACCES: permission denied, mkdir '/home/nextjs/.cache/node/corepack/v1'`. Corepack (which is bundled with Node 22) downloads pnpm to a user-specific cache on first invocation; the runner stage activated pnpm as root, so when the runtime user `nextjs` tried to invoke `pnpm start`, corepack tried to download into nextjs's home dir, which was empty/unwritable. Fix: install pnpm globally via `npm install --global pnpm@10.18.3` in the runner stage so it's on PATH for any UID.
|
||
|
||
### Prisma openssl warnings
|
||
Build emits `Prisma failed to detect the libssl/openssl version to use, and may not work as expected. Defaulting to "openssl-1.1.x".` These are harmless — Prisma 7 with the driver-adapter pattern doesn't use the native query engine binary at runtime. The warning fires during `prisma generate` checking for binary fallback compatibility. Could silence by `apt-get install -y openssl` in the builder stage; deferred.
|
||
|
||
### Image size (1.73 GB)
|
||
Bigger than ideal. Sources of bulk: full `node_modules` (we keep dev deps for `tsx` to run the worker, plus `prisma` CLI for migrations); the regular Next build vs standalone output. Optimization paths:
|
||
- **Standalone Next build** + separate slim worker image (~150 MB web, ~300 MB worker, total ~450 MB vs 1.7 GB)
|
||
- Compile worker to a single bundle with esbuild → drop tsx from runtime
|
||
- Strip dev deps from runtime — would also need to compile `scripts/seed.ts` and any other tsx-only entries
|
||
|
||
Deferred to "production prep v2" if/when image size becomes a constraint (it isn't for one-host deploys).
|
||
|
||
## Open questions
|
||
|
||
1. Customer-visible brand name (still pending)
|
||
2. Currency
|
||
3. Stripe account ownership
|
||
4. Real domain + Caddy config
|
||
5. **NEW**: image registry choice (GHCR, ECR, Docker Hub)? Defer until we deploy to more than one host.
|
||
6. **NEW**: CI provider (GitHub Actions presumably) — when the practice wants formal release discipline.
|
||
|
||
## Roadmap status
|
||
|
||
- Backend 1–4 done
|
||
- 5a + 5b + 5c.1 + 5c.2 done
|
||
- UX phases A–E done
|
||
- 5d Stripe — scaffolded; awaits real keys for live verification
|
||
- 5e reminders — done
|
||
- **Production prep — done 2026-05-03 (this session)**
|
||
|
||
**v1 is now soft-launchable.** Remaining gates before opening to real customers:
|
||
|
||
1. **Live Stripe verification in test mode** — user provides keys, we run the deposit flow end-to-end.
|
||
2. **Brand + policy decisions** — customer-visible name, ToS/cancellation copy, real domain.
|
||
3. **First production deploy** — stand up the host, run through `docs/deploy.md`, point a real domain at it.
|
||
|
||
Code-wise, nothing else is needed for soft launch.
|
||
|
||
## Recommended next step
|
||
|
||
Two-track:
|
||
|
||
- **Code track**: nothing critical. Optional polish: 24h reminder is hard-coded — make `REMINDER_LEAD_MS` configurable; add a Sentry/GlitchTip integration for prod error tracking; CI workflow with `pnpm test`/`lint`/`tsc`. Each is half-day-ish.
|
||
- **Operational track**: depends on you — Stripe keys, domain, deploy host, brand decisions.
|
||
|
||
Pause is appropriate here. Pick up when you want to either verify Stripe live, deploy somewhere, or polish a specific thing.
|
||
|
||
## How to resume
|
||
|
||
```bash
|
||
cd /Users/noise/Documents/code/touchbase
|
||
docker-compose --profile prod build # builds app + worker images (~3 min)
|
||
docker-compose --profile prod up -d # starts postgres + app + worker + caddy
|
||
curl -sf http://localhost:3000/api/health # confirms app + db reachable
|
||
docker-compose --profile prod logs -f
|
||
```
|
||
|
||
Or for a single-component test:
|
||
|
||
```bash
|
||
docker-compose up -d postgres # dev-style postgres
|
||
docker run --rm --network touchbase_default \
|
||
-e DATABASE_URL="postgresql://touchbase:touchbase@postgres:5432/touchbase_dev?schema=public" \
|
||
-e AUTH_SECRET=test -p 3001:3000 \
|
||
touchbase/app:latest
|
||
# /api/health on :3001
|
||
```
|