docs(17): create phase plan for Docker deployment
Two plans in 2 waves: - Plan 01 (Wave 1): standalone output, DB pool limit, remove @vercel/blob, health endpoint - Plan 02 (Wave 2): Dockerfile, docker-compose.yml, .dockerignore, .env.production.example Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
170
.planning/phases/17-docker-deployment/17-01-PLAN.md
Normal file
170
.planning/phases/17-docker-deployment/17-01-PLAN.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
phase: 17-docker-deployment
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- teressa-copeland-homes/next.config.ts
|
||||
- teressa-copeland-homes/src/lib/db/index.ts
|
||||
- teressa-copeland-homes/package.json
|
||||
- teressa-copeland-homes/src/app/api/health/route.ts
|
||||
autonomous: true
|
||||
requirements: [DEPLOY-01, DEPLOY-02, DEPLOY-04]
|
||||
must_haves:
|
||||
truths:
|
||||
- "next.config.ts has output: 'standalone' so Next.js produces a self-contained server.js"
|
||||
- "Database connection pool is limited to 5 connections to avoid Neon free tier exhaustion"
|
||||
- "@vercel/blob is removed from dependencies (dead dependency)"
|
||||
- "GET /api/health returns 200 with db connectivity check"
|
||||
artifacts:
|
||||
- path: "teressa-copeland-homes/next.config.ts"
|
||||
provides: "Standalone output config"
|
||||
contains: "output: 'standalone'"
|
||||
- path: "teressa-copeland-homes/src/lib/db/index.ts"
|
||||
provides: "Pool-limited database client"
|
||||
contains: "max: 5"
|
||||
- path: "teressa-copeland-homes/src/app/api/health/route.ts"
|
||||
provides: "Health check endpoint"
|
||||
exports: ["GET"]
|
||||
key_links:
|
||||
- from: "teressa-copeland-homes/src/app/api/health/route.ts"
|
||||
to: "teressa-copeland-homes/src/lib/db/index.ts"
|
||||
via: "db import for SELECT 1"
|
||||
pattern: "import.*db.*from"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Prepare the application codebase for Docker deployment by making four targeted code changes: enable standalone output, limit the database connection pool, remove the dead @vercel/blob dependency, and add a health check endpoint.
|
||||
|
||||
Purpose: These changes are prerequisites for the Dockerfile and docker-compose.yml (Plan 02). Standalone output is required for the three-stage Docker build. Pool limiting prevents Neon connection exhaustion. The health endpoint enables Docker's HEALTHCHECK directive.
|
||||
|
||||
Output: Modified next.config.ts, db/index.ts, package.json; new /api/health route
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/17-docker-deployment/17-CONTEXT.md
|
||||
|
||||
@teressa-copeland-homes/next.config.ts
|
||||
@teressa-copeland-homes/src/lib/db/index.ts
|
||||
@teressa-copeland-homes/package.json
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Enable standalone output, limit DB pool, remove @vercel/blob</name>
|
||||
<files>teressa-copeland-homes/next.config.ts, teressa-copeland-homes/src/lib/db/index.ts, teressa-copeland-homes/package.json</files>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/next.config.ts
|
||||
- teressa-copeland-homes/src/lib/db/index.ts
|
||||
- teressa-copeland-homes/package.json
|
||||
</read_first>
|
||||
<action>
|
||||
1. **next.config.ts** (per D-05): Add `output: 'standalone'` to the NextConfig object. Keep existing `transpilePackages` and `serverExternalPackages` intact. Result:
|
||||
```typescript
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
transpilePackages: ['react-pdf', 'pdfjs-dist'],
|
||||
serverExternalPackages: ['@napi-rs/canvas'],
|
||||
};
|
||||
```
|
||||
|
||||
2. **src/lib/db/index.ts** (per D-08): Change `postgres(url)` to `postgres(url, { max: 5 })` in the `createDb` function. This limits the connection pool to 5 connections, leaving headroom on Neon's free tier (10 max). Change line 12 from:
|
||||
```typescript
|
||||
const client = postgres(url);
|
||||
```
|
||||
to:
|
||||
```typescript
|
||||
const client = postgres(url, { max: 5 });
|
||||
```
|
||||
|
||||
3. **package.json** (per D-09): Remove `@vercel/blob` from dependencies. Run `npm uninstall @vercel/blob` from the `teressa-copeland-homes` directory. This removes the dead dependency that is imported nowhere in the codebase. Verify no import of `@vercel/blob` exists in src/ after removal.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && grep -q "output: 'standalone'" next.config.ts && grep -q "max: 5" src/lib/db/index.ts && ! grep -q "@vercel/blob" package.json && echo "PASS"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep "output: 'standalone'" teressa-copeland-homes/next.config.ts` returns a match
|
||||
- `grep "max: 5" teressa-copeland-homes/src/lib/db/index.ts` returns a match
|
||||
- `grep "@vercel/blob" teressa-copeland-homes/package.json` returns NO match
|
||||
- `grep -r "@vercel/blob" teressa-copeland-homes/src/` returns NO match
|
||||
- `transpilePackages` and `serverExternalPackages` still present in next.config.ts
|
||||
</acceptance_criteria>
|
||||
<done>next.config.ts has standalone output, db pool is limited to 5, @vercel/blob removed from package.json</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create /api/health endpoint with DB connectivity check</name>
|
||||
<files>teressa-copeland-homes/src/app/api/health/route.ts</files>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/src/lib/db/index.ts
|
||||
- teressa-copeland-homes/src/lib/db/schema.ts (first 20 lines, for import pattern)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `src/app/api/health/route.ts` (per D-10). This is a public endpoint (no auth) that:
|
||||
- Runs `SELECT 1` via the Drizzle db client using `db.execute(sql\`SELECT 1\`)`
|
||||
- Returns `{ ok: true, db: 'connected' }` with status 200 on success
|
||||
- Returns `{ ok: false, error: message }` with status 503 on failure
|
||||
- Wraps the DB call in try/catch
|
||||
|
||||
Full file content:
|
||||
```typescript
|
||||
import { db } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await db.execute(sql`SELECT 1`);
|
||||
return Response.json({ ok: true, db: 'connected' });
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
return Response.json({ ok: false, error: message }, { status: 503 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
No auth check — this endpoint is intentionally public so Docker HEALTHCHECK can reach it without credentials.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && test -f src/app/api/health/route.ts && grep -q "SELECT 1" src/app/api/health/route.ts && grep -q "export async function GET" src/app/api/health/route.ts && echo "PASS"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File exists at `teressa-copeland-homes/src/app/api/health/route.ts`
|
||||
- File exports a GET function
|
||||
- File contains `SELECT 1` database check
|
||||
- File returns `{ ok: true, db: 'connected' }` on success
|
||||
- File returns status 503 on failure
|
||||
- No auth import or session check present
|
||||
</acceptance_criteria>
|
||||
<done>GET /api/health endpoint exists, runs SELECT 1 via Drizzle, returns 200/503 based on DB reachability</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
All code changes compile:
|
||||
```bash
|
||||
cd teressa-copeland-homes && npx tsc --noEmit
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- next.config.ts has `output: 'standalone'` alongside existing config
|
||||
- db/index.ts creates postgres client with `{ max: 5 }`
|
||||
- @vercel/blob removed from package.json dependencies
|
||||
- /api/health route exists and checks DB connectivity
|
||||
- `npx tsc --noEmit` passes
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/17-docker-deployment/17-01-SUMMARY.md`
|
||||
</output>
|
||||
343
.planning/phases/17-docker-deployment/17-02-PLAN.md
Normal file
343
.planning/phases/17-docker-deployment/17-02-PLAN.md
Normal file
@@ -0,0 +1,343 @@
|
||||
---
|
||||
phase: 17-docker-deployment
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["17-01"]
|
||||
files_modified:
|
||||
- teressa-copeland-homes/Dockerfile
|
||||
- teressa-copeland-homes/docker-compose.yml
|
||||
- teressa-copeland-homes/.dockerignore
|
||||
- teressa-copeland-homes/.env.production.example
|
||||
- teressa-copeland-homes/.gitignore
|
||||
autonomous: true
|
||||
requirements: [DEPLOY-01, DEPLOY-02, DEPLOY-03, DEPLOY-04, DEPLOY-05]
|
||||
must_haves:
|
||||
truths:
|
||||
- "docker compose up starts the app and /api/health is reachable"
|
||||
- "SMTP secrets are injected at runtime via env_file, not baked into image"
|
||||
- "Uploaded PDFs persist across container restarts via named volume"
|
||||
- "Dockerfile uses node:20-slim with --platform linux/amd64 on all 3 FROM lines"
|
||||
- "seeds/forms directory is available inside the container for form library imports"
|
||||
artifacts:
|
||||
- path: "teressa-copeland-homes/Dockerfile"
|
||||
provides: "Three-stage Docker build"
|
||||
contains: "--platform linux/amd64"
|
||||
- path: "teressa-copeland-homes/docker-compose.yml"
|
||||
provides: "Production compose config"
|
||||
contains: "env_file"
|
||||
- path: "teressa-copeland-homes/.dockerignore"
|
||||
provides: "Build context exclusions"
|
||||
contains: "node_modules"
|
||||
- path: "teressa-copeland-homes/.env.production.example"
|
||||
provides: "Env var template for production"
|
||||
contains: "DATABASE_URL"
|
||||
key_links:
|
||||
- from: "teressa-copeland-homes/docker-compose.yml"
|
||||
to: ".env.production"
|
||||
via: "env_file directive"
|
||||
pattern: "env_file"
|
||||
- from: "teressa-copeland-homes/docker-compose.yml"
|
||||
to: "teressa-copeland-homes/Dockerfile"
|
||||
via: "build context"
|
||||
pattern: "build:"
|
||||
- from: "teressa-copeland-homes/Dockerfile"
|
||||
to: "server.js"
|
||||
via: "CMD directive"
|
||||
pattern: 'CMD.*node.*server.js'
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create all Docker deployment files: Dockerfile (three-stage, node:20-slim, linux/amd64), docker-compose.yml (env_file secrets, named uploads volume, SMTP DNS fix), .dockerignore, .env.production.example, and update .gitignore.
|
||||
|
||||
Purpose: This is the deployment infrastructure that makes the app run reliably on the production home server. Secrets are injected at runtime, uploads persist across restarts, and SMTP DNS is fixed.
|
||||
|
||||
Output: Dockerfile, docker-compose.yml, .dockerignore, .env.production.example, updated .gitignore
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/17-docker-deployment/17-CONTEXT.md
|
||||
@.planning/phases/17-docker-deployment/17-01-SUMMARY.md
|
||||
@.planning/research/ARCHITECTURE.md
|
||||
@.planning/research/PITFALLS.md
|
||||
|
||||
@teressa-copeland-homes/next.config.ts
|
||||
@teressa-copeland-homes/.gitignore
|
||||
|
||||
<interfaces>
|
||||
<!-- Key facts the executor needs about standalone mode and uploads -->
|
||||
|
||||
Standalone output (from Plan 01):
|
||||
- next.config.ts has `output: 'standalone'`
|
||||
- `npm run build` produces `.next/standalone/server.js` + `.next/static/`
|
||||
- In the runner stage, standalone files are copied to WORKDIR /app
|
||||
- `process.cwd()` in the container = `/app`
|
||||
- Therefore uploads path = `/app/uploads` (correct for volume mount)
|
||||
|
||||
Seeds directory (runtime dependency):
|
||||
- `src/app/api/documents/route.ts` reads from `path.join(process.cwd(), 'seeds', 'forms')` at runtime
|
||||
- This is the form library import feature — copies PDF from seeds/forms/ to uploads/
|
||||
- The seeds/forms/ directory MUST be copied into the runner stage at `/app/seeds/forms/`
|
||||
- Without it, importing forms from the library returns 404
|
||||
|
||||
Uploads path in all API routes:
|
||||
- `const UPLOADS_DIR = path.join(process.cwd(), 'uploads')` — used in 8+ route files
|
||||
- In standalone container: process.cwd() = /app, so UPLOADS_DIR = /app/uploads
|
||||
- Volume mount at /app/uploads is correct — no code changes needed
|
||||
|
||||
APP_BASE_URL (already renamed from NEXT_PUBLIC_BASE_URL in Phase 15):
|
||||
- Server-only env var, read at runtime — NOT baked into build
|
||||
- Used in send/route.ts and sign/[token]/route.ts for signing URLs
|
||||
- Must be in .env.production (e.g., APP_BASE_URL=https://teressacopelandhomes.com)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create Dockerfile, .dockerignore, and .env.production.example</name>
|
||||
<files>teressa-copeland-homes/Dockerfile, teressa-copeland-homes/.dockerignore, teressa-copeland-homes/.env.production.example</files>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/next.config.ts (confirm standalone is set from Plan 01)
|
||||
- teressa-copeland-homes/package.json (confirm build script name)
|
||||
- teressa-copeland-homes/.gitignore
|
||||
</read_first>
|
||||
<action>
|
||||
**1. Create `teressa-copeland-homes/Dockerfile`** (per D-01, D-04):
|
||||
|
||||
Three-stage build. ALL three FROM lines use `--platform linux/amd64`. Uses `node:20-slim` (Debian, NOT Alpine — required for @napi-rs/canvas glibc compatibility). The runner stage copies `seeds/forms/` for the form library import feature.
|
||||
|
||||
Full Dockerfile content:
|
||||
```dockerfile
|
||||
# Stage 1: Install production dependencies
|
||||
FROM --platform=linux/amd64 node:20-slim AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Stage 2: Build the application
|
||||
FROM --platform=linux/amd64 node:20-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ENV NODE_ENV=production
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Production runner
|
||||
FROM --platform=linux/amd64 node:20-slim AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy standalone server and static assets
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
# Copy seeds/forms for form library import feature (runtime dependency)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/seeds ./seeds
|
||||
|
||||
# Create uploads directory (will be mounted as volume)
|
||||
RUN mkdir -p uploads && chown nextjs:nodejs uploads
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
Key decisions in this Dockerfile:
|
||||
- `--platform=linux/amd64` on all 3 FROM lines (D-01) for x86_64 home server
|
||||
- `node:20-slim` not Alpine (D-01, glibc required for @napi-rs/canvas)
|
||||
- `npm ci --omit=dev` in deps stage (D-04) — no devDependencies in prod
|
||||
- Non-root user `nextjs:nodejs` (D-04)
|
||||
- `seeds/` copied for runtime form library access
|
||||
- `uploads/` dir pre-created and owned by nextjs user
|
||||
- HEALTHCHECK uses wget (available in Debian slim, curl is not)
|
||||
- NO secrets in any ARG or ENV line (D-02)
|
||||
|
||||
**2. Create `teressa-copeland-homes/.dockerignore`**:
|
||||
|
||||
```
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
uploads/
|
||||
*.md
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
npm-debug.log*
|
||||
scripts/*.png
|
||||
drizzle/meta
|
||||
```
|
||||
|
||||
Note: `seeds/` is NOT in .dockerignore — it is needed in the build context for COPY. `drizzle/` is NOT excluded — migration files may be useful for reference (though migrations run from host per D-02).
|
||||
|
||||
**3. Create `teressa-copeland-homes/.env.production.example`** (per D-03):
|
||||
|
||||
```
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require
|
||||
|
||||
# Authentication
|
||||
SIGNING_JWT_SECRET=your-jwt-secret-here
|
||||
AUTH_SECRET=your-auth-secret-here
|
||||
AGENT_EMAIL=agent@example.com
|
||||
AGENT_PASSWORD=your-agent-password
|
||||
|
||||
# SMTP (email delivery)
|
||||
CONTACT_EMAIL_USER=your-smtp-username
|
||||
CONTACT_EMAIL_PASS=your-smtp-password
|
||||
CONTACT_SMTP_HOST=smtp.example.com
|
||||
CONTACT_SMTP_PORT=465
|
||||
|
||||
# OpenAI (AI field placement)
|
||||
OPENAI_API_KEY=sk-your-openai-key
|
||||
|
||||
# Application
|
||||
APP_BASE_URL=https://yourdomain.com
|
||||
```
|
||||
|
||||
Exactly 11 vars as specified in D-03. No BLOB_READ_WRITE_TOKEN (removed in Plan 01). No SKYSLOPE/URE credentials (dev-only).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && test -f Dockerfile && test -f .dockerignore && test -f .env.production.example && grep -c "platform=linux/amd64" Dockerfile | grep -q "3" && grep -q "node:20-slim" Dockerfile && grep -q "CMD.*node.*server.js" Dockerfile && grep -q "DATABASE_URL" .env.production.example && grep -q "APP_BASE_URL" .env.production.example && ! grep -q "BLOB_READ_WRITE_TOKEN" .env.production.example && echo "PASS"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `teressa-copeland-homes/Dockerfile` exists with exactly 3 `--platform=linux/amd64` FROM lines
|
||||
- All FROM lines use `node:20-slim` (not Alpine)
|
||||
- CMD is `["node", "server.js"]`
|
||||
- No ARG or ENV lines containing secret values
|
||||
- HEALTHCHECK directive present pointing to /api/health
|
||||
- `seeds/` directory is copied in runner stage
|
||||
- `teressa-copeland-homes/.dockerignore` exists, excludes node_modules, .next, .env*, uploads/
|
||||
- `teressa-copeland-homes/.env.production.example` exists with all 11 required vars
|
||||
- No BLOB_READ_WRITE_TOKEN in .env.production.example
|
||||
- APP_BASE_URL present (not NEXT_PUBLIC_BASE_URL)
|
||||
</acceptance_criteria>
|
||||
<done>Dockerfile with three-stage build, .dockerignore, and .env.production.example all created with correct content</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create docker-compose.yml and update .gitignore</name>
|
||||
<files>teressa-copeland-homes/docker-compose.yml, teressa-copeland-homes/.gitignore</files>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/Dockerfile (just created in Task 1)
|
||||
- teressa-copeland-homes/.gitignore
|
||||
</read_first>
|
||||
<action>
|
||||
**1. Create `teressa-copeland-homes/docker-compose.yml`** (per D-06, D-07):
|
||||
|
||||
Full docker-compose.yml content:
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
- .env.production
|
||||
environment:
|
||||
- NODE_OPTIONS=--dns-result-order=ipv4first
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
|
||||
volumes:
|
||||
uploads:
|
||||
```
|
||||
|
||||
Key decisions:
|
||||
- `env_file: .env.production` — secrets injected at runtime, not baked (D-02, D-06)
|
||||
- `dns: ["8.8.8.8", "1.1.1.1"]` — SMTP DNS fix, prevents EAI_AGAIN errors (D-07)
|
||||
- `environment: NODE_OPTIONS: --dns-result-order=ipv4first` — companion DNS fix (D-07)
|
||||
- Named volume `uploads:/app/uploads` — persists PDFs across container restarts (D-06)
|
||||
- Volume mounts at `/app/uploads` which matches `process.cwd() + '/uploads'` in standalone mode
|
||||
- `restart: unless-stopped` (D-06)
|
||||
- Port 3000:3000 (D-06)
|
||||
- No `healthcheck:` in compose — the Dockerfile already has HEALTHCHECK
|
||||
- Build context is `.` (the teressa-copeland-homes directory itself)
|
||||
|
||||
**2. Update `teressa-copeland-homes/.gitignore`**:
|
||||
|
||||
Add `.env.production` to the existing `.env*` pattern. The existing `.env*` glob already covers it, but add an explicit comment for clarity. Also ensure `uploads/` is gitignored. Add these lines at the end of the file:
|
||||
|
||||
```
|
||||
# Production uploads (Docker volume on server)
|
||||
/uploads/
|
||||
```
|
||||
|
||||
The existing `.env*` pattern on line 34 already covers `.env.production`, so no additional env line is needed. Just verify it's there.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && test -f docker-compose.yml && grep -q "env_file" docker-compose.yml && grep -q "dns" docker-compose.yml && grep -q "dns-result-order=ipv4first" docker-compose.yml && grep -q "uploads:/app/uploads" docker-compose.yml && grep -q "restart: unless-stopped" docker-compose.yml && grep -q "3000:3000" docker-compose.yml && echo "PASS"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `teressa-copeland-homes/docker-compose.yml` exists
|
||||
- Contains `env_file: .env.production` (not `environment:` for secrets)
|
||||
- Contains `dns:` array with 8.8.8.8 and 1.1.1.1
|
||||
- Contains `NODE_OPTIONS=--dns-result-order=ipv4first`
|
||||
- Contains named volume `uploads:/app/uploads`
|
||||
- Contains `restart: unless-stopped`
|
||||
- Contains port mapping `3000:3000`
|
||||
- `.gitignore` includes `/uploads/` entry
|
||||
- `.gitignore` existing `.env*` pattern covers .env.production
|
||||
</acceptance_criteria>
|
||||
<done>docker-compose.yml with env_file secrets, DNS fix, named volume, and restart policy; .gitignore updated</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Verify all Docker files are syntactically valid:
|
||||
```bash
|
||||
cd teressa-copeland-homes && docker compose config --quiet 2>&1 || echo "compose config check"
|
||||
```
|
||||
|
||||
Verify Dockerfile has exactly 3 platform-targeted FROM lines:
|
||||
```bash
|
||||
grep -c "platform=linux/amd64" Dockerfile
|
||||
```
|
||||
|
||||
Verify no secrets in Dockerfile:
|
||||
```bash
|
||||
! grep -iE "(password|secret|key=)" Dockerfile
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Dockerfile exists with 3x `--platform=linux/amd64 node:20-slim` FROM lines
|
||||
- docker-compose.yml injects secrets via env_file, not environment block
|
||||
- docker-compose.yml has DNS fix (dns array + NODE_OPTIONS)
|
||||
- Named volume mounts at /app/uploads (matches process.cwd() + '/uploads' in standalone)
|
||||
- seeds/ directory copied into runner stage for form library
|
||||
- .env.production.example has all 11 required vars, no dead vars
|
||||
- .dockerignore excludes node_modules, .next, .env*, uploads/
|
||||
- .gitignore covers .env.production and uploads/
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/17-docker-deployment/17-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user