6.8 KiB
Phase 17: Docker Deployment - Context
Gathered: 2026-04-03 Status: Ready for planning
## Phase BoundaryInfrastructure-only phase. No new features, no schema changes, no UI changes. Deliverables:
Dockerfile— three-stage,node:20-slim,--platform linux/amd64docker-compose.yml— named uploads volume,env_file, SMTP DNS fix.dockerignore— exclude dev files from imagenext.config.ts— addoutput: 'standalone'/api/healthroute — returns 200 + DB pingsrc/lib/db/index.ts— add Neon pool limit (max: 5)package.json— remove dead@vercel/blobdependency.env.production.example— template for production env vars (no real secrets)- Deployment notes (brief README or DEPLOYMENT.md) — document migration step
CPU Architecture
- D-01: All Dockerfile
FROMlines 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(ornode .next/standalone/server.js). A brief deployment note documents: "Before starting container, runDATABASE_URL=... npx drizzle-kit migratefrom the project directory."
Production Env Vars
-
D-03:
.env.production.exampleincludes 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_TOKENis 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:
- deps (
--platform linux/amd64 node:20-slim AS deps) — install production dependencies only (npm ci --omit=dev) - builder (
--platform linux/amd64 node:20-slim AS builder) — copy source + deps, runnpm run buildwithoutput: 'standalone' - runner (
--platform linux/amd64 node:20-slim AS runner) — copy.next/standalone,.next/static,public, set non-root user,CMD ["node", "server.js"]
- deps (
-
D-05:
output: 'standalone'added tonext.config.ts. Keeps existingtranspilePackagesandserverExternalPackagesconfig intact.
Docker Compose
- D-06:
docker-compose.ymlservice definition:env_file: .env.production(NOTenvironment: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 — preventsEAI_AGAINerrors 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 — adddnsarray andNODE_OPTIONSto the app service. No code changes needed in the mailer itself.
Neon Connection Pool
- D-08:
src/lib/db/index.tscurrently callspostgres(url)with nomaxparameter (default 10 connections). Change topostgres(url, { max: 5 })to leave headroom for migrations and overlapping deploys on Neon's free tier.
@vercel/blob Removal
- D-09: Remove
@vercel/blobfrompackage.jsondependencies. Research confirmed it is imported nowhere in the codebase — dead dependency.BLOB_READ_WRITE_TOKENexcluded from.env.production.example(D-03).
Health Endpoint
- D-10: New App Router route
GET /api/healthreturns{ ok: true, db: 'connected' }with status 200 when the database is reachable. Runs a lightweightSELECT 1via Drizzle. Returns{ ok: false }with status 503 if DB unreachable. No auth required.
Claude's Discretion
- Exact
.dockerignoreentries (excludenode_modules,.next,.git,uploads,.env*,*.mddocs, etc.) - Whether DEPLOYMENT.md is a separate file or a section in the existing README
- Health check
HEALTHCHECKdirective in Dockerfile (optional Docker-native health check)
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Files Being Modified / Created
teressa-copeland-homes/next.config.ts— addoutput: 'standalone'teressa-copeland-homes/src/lib/db/index.ts— add poolmax: 5teressa-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/blobdead 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— hastranspilePackagesandserverExternalPackages; nooutputfield yet- No
Dockerfile, nodocker-compose.yml, no.dockerignore— all new files .env.localexists (dev only); no.env.production— agent creates.env.production.examplesrc/lib/db/index.ts— usespostgres(url)(no pool limit)@vercel/blobinpackage.jsonbut imported nowhere
Key Constraint
@napi-rs/canvasdeclared asserverExternalPackagesin next.config.ts — this confirms it's a native binary. Must usenode:20-slim(Debian glibc), NOT Alpine.--platform linux/amd64ensures correct binary architecture.
</code_context>
## 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/uploadsOR 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.exampleis committed.
None — discussion stayed within phase scope.
Phase: 17-docker-deployment Context gathered: 2026-04-03