124 lines
11 KiB
Markdown
124 lines
11 KiB
Markdown
---
|
|
phase: 17-docker-deployment
|
|
verified: 2026-04-03T23:15:00Z
|
|
status: passed
|
|
score: 9/9 must-haves verified
|
|
---
|
|
|
|
# Phase 17: Docker Deployment Verification Report
|
|
|
|
**Phase Goal:** The application runs reliably in a Docker container on the production server — secrets are injected at runtime, email delivers correctly, uploaded files survive container restarts, and a health check confirms database connectivity
|
|
**Verified:** 2026-04-03T23:15:00Z
|
|
**Status:** PASSED
|
|
**Re-verification:** No — initial verification
|
|
|
|
## Goal Achievement
|
|
|
|
### Observable Truths
|
|
|
|
| # | Truth | Status | Evidence |
|
|
|----|-------------------------------------------------------------------------------------------|------------|----------------------------------------------------------------------------------------------------|
|
|
| 1 | next.config.ts has `output: 'standalone'` enabling self-contained server.js | VERIFIED | `output: 'standalone'` present on line 4 of next.config.ts |
|
|
| 2 | Dockerfile uses node:20-slim with --platform=linux/amd64 on all 3 FROM lines | VERIFIED | All 3 FROM lines confirmed; `grep -c "platform=linux/amd64" Dockerfile` returns 3 |
|
|
| 3 | SMTP secrets are injected at runtime via env_file, not baked into image | VERIFIED | docker-compose.yml uses `env_file: .env.production`; no secrets in any Dockerfile ARG/ENV line |
|
|
| 4 | Uploaded PDFs persist across container restarts via named Docker volume | VERIFIED | Named volume `uploads:/app/uploads` in docker-compose.yml matches standalone `process.cwd()+/uploads` |
|
|
| 5 | GET /api/health returns 200 OK when database is reachable, 503 when not | VERIFIED | Route runs `SELECT 1` via Drizzle, returns `{ok:true,db:"connected"}` or `{ok:false,error}` 503 |
|
|
| 6 | Database connection pool is limited to 5 connections | VERIFIED | `postgres(url, { max: 5 })` confirmed in src/lib/db/index.ts line 12 |
|
|
| 7 | SMTP DNS fix prevents EAI_AGAIN errors in container | VERIFIED | `dns: [8.8.8.8, 1.1.1.1]` + `NODE_OPTIONS=--dns-result-order=ipv4first` in docker-compose.yml |
|
|
| 8 | seeds/forms directory is available inside the container for form library imports | VERIFIED | Dockerfile runner stage: `COPY --from=builder /app/seeds ./seeds` |
|
|
| 9 | DEPLOYMENT.md documents the full deploy workflow including migration step | VERIFIED | File covers: env setup, `drizzle-kit migrate`, `docker compose up -d --build`, health check |
|
|
|
|
**Score:** 9/9 truths verified
|
|
|
|
### Required Artifacts
|
|
|
|
| Artifact | Expected | Status | Details |
|
|
|-----------------------------------------------|---------------------------------------|------------|----------------------------------------------------------------------|
|
|
| `teressa-copeland-homes/Dockerfile` | Three-stage Docker build | VERIFIED | 3x `--platform=linux/amd64 node:20-slim`; seeds copied; HEALTHCHECK |
|
|
| `teressa-copeland-homes/docker-compose.yml` | Production compose config | VERIFIED | env_file, DNS fix, named volume, restart policy, port 3000 |
|
|
| `teressa-copeland-homes/.dockerignore` | Build context exclusions | VERIFIED | Excludes node_modules, .next, .git, .env*, uploads/, *.md |
|
|
| `teressa-copeland-homes/.env.production.example` | Runtime env var template | VERIFIED | Exactly 11 vars; APP_BASE_URL (not NEXT_PUBLIC_BASE_URL); no BLOB token |
|
|
| `teressa-copeland-homes/DEPLOYMENT.md` | Full deploy guide with migration step | VERIFIED | Covers all 4 steps; committed to git |
|
|
| `teressa-copeland-homes/next.config.ts` | Standalone output config | VERIFIED | `output: 'standalone'` alongside existing transpile and external pkgs |
|
|
| `teressa-copeland-homes/src/lib/db/index.ts` | Pool-limited database client | VERIFIED | `postgres(url, { max: 5 })` lazy singleton pattern intact |
|
|
| `teressa-copeland-homes/src/app/api/health/route.ts` | Health check endpoint | VERIFIED | Exports GET, runs SELECT 1, no auth import, 200/503 responses |
|
|
|
|
### Key Link Verification
|
|
|
|
| From | To | Via | Status | Details |
|
|
|---------------------------------------|---------------------------|----------------------------|------------|-----------------------------------------------------------------|
|
|
| `docker-compose.yml` | `.env.production` | `env_file` directive | WIRED | `env_file: .env.production` present |
|
|
| `docker-compose.yml` | `Dockerfile` | `build:` context | WIRED | `build: context: . dockerfile: Dockerfile` present |
|
|
| `Dockerfile` | `server.js` | CMD directive | WIRED | `CMD ["node", "server.js"]` confirmed |
|
|
| `src/app/api/health/route.ts` | `src/lib/db/index.ts` | `import { db } from '@/lib/db'` | WIRED | Import present; `db.execute(sql\`SELECT 1\`)` called |
|
|
| `docker-compose.yml` uploads volume | `/app/uploads` | named volume mount | WIRED | `uploads:/app/uploads` matches `process.cwd()+'/uploads'` in standalone |
|
|
|
|
### Data-Flow Trace (Level 4)
|
|
|
|
Not applicable — this phase produces infrastructure files (Dockerfile, docker-compose.yml, config), not components that render dynamic data. The health endpoint is a simple passthrough probe, not a data-rendering artifact.
|
|
|
|
### Behavioral Spot-Checks
|
|
|
|
Step 7b: SKIPPED — no running server available. The health endpoint requires DATABASE_URL to be set. Static code verification is sufficient for infrastructure artifacts (Dockerfile, compose, config files).
|
|
|
|
| Behavior | Command | Result | Status |
|
|
|---------------------------------------------|-------------------------------------------|---------|--------|
|
|
| health route exports GET | file read + grep | confirmed | PASS |
|
|
| health route calls SELECT 1 | `grep "SELECT 1" route.ts` | confirmed | PASS |
|
|
| health route returns 503 on failure | `grep "503" route.ts` | confirmed | PASS |
|
|
| Dockerfile has 3 platform-targeted FROM lines | `grep -c "platform=linux/amd64" Dockerfile` | 3 | PASS |
|
|
| No secrets baked into Dockerfile | grep for PASSWORD/SECRET/KEY= in Dockerfile | nothing found | PASS |
|
|
|
|
### Requirements Coverage
|
|
|
|
| Requirement | Source Plan | Description | Status | Evidence |
|
|
|-------------|-------------|----------------------------------------------------------------------------------------------|-----------|--------------------------------------------------------------|
|
|
| DEPLOY-01 | 17-01, 17-02 | App runs in Docker with production docker-compose.yml (node:20-slim, three-stage, not Alpine) | SATISFIED | Dockerfile confirmed 3x node:20-slim + --platform=linux/amd64 |
|
|
| DEPLOY-02 | 17-01, 17-02 | All secrets injected at runtime via env_file — not baked into image | SATISFIED | env_file directive in compose; no secrets in Dockerfile |
|
|
| DEPLOY-03 | 17-02 | Email delivery works from container (SMTP connects to external SMTP server) | SATISFIED | DNS fix (dns array + NODE_OPTIONS) in compose; SMTP vars in .env.production.example wired to signing-mailer.tsx and contact-mailer.ts |
|
|
| DEPLOY-04 | 17-01 | GET /api/health returns 200 OK when database is reachable | SATISFIED | Route exists, SELECT 1 via Drizzle, returns {ok:true} or 503 |
|
|
| DEPLOY-05 | 17-02 | Uploaded PDFs persist across container restarts (named Docker volume for uploads dir) | SATISFIED | Named volume `uploads:/app/uploads` in compose |
|
|
|
|
### Anti-Patterns Found
|
|
|
|
| File | Line | Pattern | Severity | Impact |
|
|
|------|------|---------|----------|--------|
|
|
| None | — | — | — | — |
|
|
|
|
No TODO/FIXME/placeholder patterns found in phase-17 artifacts. No hardcoded secrets found in Dockerfile or compose files.
|
|
|
|
One notable observation: the `.dockerignore` contains `.env*` which would exclude `.env.production.example` from the Docker build context. This is correct — the example file is not needed inside the container image. It is committed to git for operator reference (confirmed: `git ls-files .env.production.example` returns the file). Similarly `*.md` excludes `DEPLOYMENT.md` from the build context, which is intentional.
|
|
|
|
### Human Verification Required
|
|
|
|
### 1. SMTP Delivery from Container
|
|
|
|
**Test:** On the production server, after deploying with real SMTP credentials, submit the contact form or trigger a document signing. Confirm the email arrives in the recipient's inbox.
|
|
**Expected:** Email delivered successfully; no EAI_AGAIN DNS errors in container logs.
|
|
**Why human:** Cannot test SMTP connectivity without a live container connected to a real SMTP server. The DNS fix (dns array + NODE_OPTIONS) addresses the known Docker SMTP DNS issue, but confirmation requires an actual send from the deployed container.
|
|
|
|
### 2. Upload Persistence Across Container Restart
|
|
|
|
**Test:** Upload a PDF document via the portal, then run `docker compose restart`. Confirm the uploaded file is still accessible after restart.
|
|
**Expected:** File still present and downloadable after restart.
|
|
**Why human:** Named volume wiring is verified statically. Actual persistence requires running the container and restarting it.
|
|
|
|
### 3. Health Check Endpoint Live Response
|
|
|
|
**Test:** After `docker compose up -d --build` on the server, run `curl http://localhost:3000/api/health`.
|
|
**Expected:** `{"ok":true,"db":"connected"}` with HTTP 200.
|
|
**Why human:** Requires live DATABASE_URL pointed at Neon; cannot test without the running container connected to the database.
|
|
|
|
---
|
|
|
|
## Gaps Summary
|
|
|
|
No gaps found. All 9 observable truths verified. All 5 requirements (DEPLOY-01 through DEPLOY-05) are satisfied by concrete code artifacts. All key links are wired. All commits (fa7d6a9, 57326d7, e83ced5, a107970, 72c23f8) exist in git log.
|
|
|
|
The three human verification items above are runtime behaviors that require a live container on the production server — they cannot be confirmed statically but there are no code-level blockers preventing them from succeeding.
|
|
|
|
---
|
|
|
|
_Verified: 2026-04-03T23:15:00Z_
|
|
_Verifier: Claude (gsd-verifier)_
|