Files
2026-04-03 16:41:12 -06:00

152 lines
6.8 KiB
Markdown

# Phase 17: Docker Deployment - Context
**Gathered:** 2026-04-03
**Status:** Ready for planning
<domain>
## Phase Boundary
Infrastructure-only phase. No new features, no schema changes, no UI changes. Deliverables:
1. `Dockerfile` — three-stage, `node:20-slim`, `--platform linux/amd64`
2. `docker-compose.yml` — named uploads volume, `env_file`, SMTP DNS fix
3. `.dockerignore` — exclude dev files from image
4. `next.config.ts` — add `output: 'standalone'`
5. `/api/health` route — returns 200 + DB ping
6. `src/lib/db/index.ts` — add Neon pool limit (`max: 5`)
7. `package.json` — remove dead `@vercel/blob` dependency
8. `.env.production.example` — template for production env vars (no real secrets)
9. Deployment notes (brief README or DEPLOYMENT.md) — document migration step
</domain>
<decisions>
## Implementation Decisions
### CPU Architecture
- **D-01:** All Dockerfile `FROM` lines use `--platform linux/amd64`. The home server is x86_64. Mac (ARM) builds correctly cross-compile with this flag. Applies to all three stages (deps, builder, runner).
### Migration Strategy
- **D-02:** Database migrations are run from the developer's machine before deploying the container — **not** in the container entrypoint or CMD. The container CMD is simply `node server.js` (or `node .next/standalone/server.js`). A brief deployment note documents: "Before starting container, run `DATABASE_URL=... npx drizzle-kit migrate` from the project directory."
### Production Env Vars
- **D-03:** `.env.production.example` includes ONLY the vars needed for the running app. Skyslope/URE credentials (`SKYSLOPE_LAST_NAME`, `SKYSLOPE_NRDS_ID`, `URE_USERNAME`, `URE_PASSWORD`) are excluded — they are dev-only forms scraping tools, not needed at runtime. `BLOB_READ_WRITE_TOKEN` is also excluded (dead dependency being removed).
**Required production vars:**
```
DATABASE_URL=
SIGNING_JWT_SECRET=
AUTH_SECRET=
AGENT_EMAIL=
AGENT_PASSWORD=
CONTACT_EMAIL_USER=
CONTACT_EMAIL_PASS=
CONTACT_SMTP_HOST=
CONTACT_SMTP_PORT=
OPENAI_API_KEY=
APP_BASE_URL=
```
### Dockerfile Structure
- **D-04:** Three-stage build pattern:
1. **deps** (`--platform linux/amd64 node:20-slim AS deps`) — install production dependencies only (`npm ci --omit=dev`)
2. **builder** (`--platform linux/amd64 node:20-slim AS builder`) — copy source + deps, run `npm run build` with `output: 'standalone'`
3. **runner** (`--platform linux/amd64 node:20-slim AS runner`) — copy `.next/standalone`, `.next/static`, `public`, set non-root user, `CMD ["node", "server.js"]`
- **D-05:** `output: 'standalone'` added to `next.config.ts`. Keeps existing `transpilePackages` and `serverExternalPackages` config intact.
### Docker Compose
- **D-06:** `docker-compose.yml` service definition:
- `env_file: .env.production` (NOT `environment:` key — secrets stay out of compose file)
- Named volume `uploads:/app/uploads` (persistent across container restarts)
- `dns: ["8.8.8.8", "1.1.1.1"]` (SMTP DNS fix — prevents `EAI_AGAIN` errors on external SMTP)
- `environment: NODE_OPTIONS: --dns-result-order=ipv4first` (companion to DNS fix)
- Port mapping `3000:3000`
- `restart: unless-stopped`
### SMTP DNS Fix
- **D-07:** Docker's internal DNS resolver intermittently fails on external hostnames (`EAI_AGAIN`). Fix is already documented — add `dns` array and `NODE_OPTIONS` to the app service. No code changes needed in the mailer itself.
### Neon Connection Pool
- **D-08:** `src/lib/db/index.ts` currently calls `postgres(url)` with no `max` parameter (default 10 connections). Change to `postgres(url, { max: 5 })` to leave headroom for migrations and overlapping deploys on Neon's free tier.
### @vercel/blob Removal
- **D-09:** Remove `@vercel/blob` from `package.json` dependencies. Research confirmed it is imported nowhere in the codebase — dead dependency. `BLOB_READ_WRITE_TOKEN` excluded from `.env.production.example` (D-03).
### Health Endpoint
- **D-10:** New App Router route `GET /api/health` returns `{ ok: true, db: 'connected' }` with status 200 when the database is reachable. Runs a lightweight `SELECT 1` via Drizzle. Returns `{ ok: false }` with status 503 if DB unreachable. No auth required.
### Claude's Discretion
- Exact `.dockerignore` entries (exclude `node_modules`, `.next`, `.git`, `uploads`, `.env*`, `*.md` docs, etc.)
- Whether DEPLOYMENT.md is a separate file or a section in the existing README
- Health check `HEALTHCHECK` directive in Dockerfile (optional Docker-native health check)
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Files Being Modified / Created
- `teressa-copeland-homes/next.config.ts` — add `output: 'standalone'`
- `teressa-copeland-homes/src/lib/db/index.ts` — add pool `max: 5`
- `teressa-copeland-homes/package.json` — remove `@vercel/blob`
- New: `teressa-copeland-homes/Dockerfile`
- New: `teressa-copeland-homes/docker-compose.yml`
- New: `teressa-copeland-homes/.dockerignore`
- New: `teressa-copeland-homes/src/app/api/health/route.ts`
- New: `teressa-copeland-homes/.env.production.example`
### Research
- `.planning/research/ARCHITECTURE.md` — Docker Compose patterns, standalone output, Neon pool
- `.planning/research/PITFALLS.md` — Alpine incompatibility, NEXT_PUBLIC_BASE_URL build-time baking, `@vercel/blob` dead dep, DNS issues
### Schema (DB connection)
- `teressa-copeland-homes/src/lib/db/index.ts` — current db client setup
</canonical_refs>
<code_context>
## Existing Code Insights
### Current State
- `next.config.ts` — has `transpilePackages` and `serverExternalPackages`; no `output` field yet
- No `Dockerfile`, no `docker-compose.yml`, no `.dockerignore` — all new files
- `.env.local` exists (dev only); no `.env.production` — agent creates `.env.production.example`
- `src/lib/db/index.ts` — uses `postgres(url)` (no pool limit)
- `@vercel/blob` in `package.json` but imported nowhere
### Key Constraint
- `@napi-rs/canvas` declared as `serverExternalPackages` in next.config.ts — this confirms it's a native binary. Must use `node:20-slim` (Debian glibc), NOT Alpine. `--platform linux/amd64` ensures correct binary architecture.
</code_context>
<specifics>
## Specific Ideas
- The uploads directory is currently `path.join(process.cwd(), 'uploads')` in the app code. In standalone mode, `process.cwd()` points to `.next/standalone/`. The Docker volume should mount at `/app/.next/standalone/uploads` OR the code should reference an absolute path via an env var. The planner should check this and use the correct mount path.
- `.env.production` (the actual secrets file) is created by Teressa on the server — it is never committed. Only `.env.production.example` is committed.
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope.
</deferred>
---
*Phase: 17-docker-deployment*
*Context gathered: 2026-04-03*