152 lines
6.8 KiB
Markdown
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*
|