Files
red/.planning/phases/17-docker-deployment/17-CONTEXT.md
2026-04-03 16:41:12 -06:00

6.8 KiB

Phase 17: Docker Deployment - Context

Gathered: 2026-04-03 Status: Ready for planning

## 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
## 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)

<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>

## 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.
## Deferred Ideas

None — discussion stayed within phase scope.


Phase: 17-docker-deployment Context gathered: 2026-04-03