diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 41bf601..da6b25a 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -373,12 +373,11 @@ Plans:
3. The `APP_BASE_URL` variable (renamed from `NEXT_PUBLIC_BASE_URL`) is injected at container runtime — signing link URLs in emails contain the correct production domain, not localhost
4. Uploaded PDF files written inside the container persist after `docker compose down && docker compose up` (named Docker volume mounted at /app/uploads)
5. The Docker image uses `node:20-slim` (Debian-based) — `@napi-rs/canvas` native binary loads without errors at container startup
-**Plans**: 3 plans
+**Plans**: 2 plans
Plans:
-- [x] 15-01-PLAN.md — Token utility extensions (signerEmail param, signer-download JWT), sendSignerCompletionEmail mailer, public signer download route
-- [ ] 15-02-PLAN.md — Send route rewrite: multi-signer token loop with Promise.all dispatch, legacy single-signer fallback, APP_BASE_URL rename
-- [ ] 15-03-PLAN.md — Sign handler rewrite: GET signer-filtered fields, POST signer-scoped operations + accumulate PDF + atomic completion + notifications
+- [ ] 17-01-PLAN.md — Enable standalone output, limit DB pool to 5, remove @vercel/blob, add /api/health endpoint
+- [ ] 17-02-PLAN.md — Dockerfile (three-stage, node:20-slim, linux/amd64), docker-compose.yml (env_file, DNS fix, named volume), .dockerignore, .env.production.example
**UI hint**: no
## Progress
@@ -406,4 +405,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 14. Multi-Signer Schema | v1.2 | 1/1 | Complete | 2026-04-03 |
| 15. Multi-Signer Backend | v1.2 | 3/3 | Complete | 2026-04-03 |
| 16. Multi-Signer UI | v1.2 | 1/4 | Complete | 2026-04-03 |
-| 17. Docker Deployment | v1.2 | 0/TBD | Not started | - |
+| 17. Docker Deployment | v1.2 | 0/2 | Not started | - |
diff --git a/.planning/phases/17-docker-deployment/17-01-PLAN.md b/.planning/phases/17-docker-deployment/17-01-PLAN.md
new file mode 100644
index 0000000..17ad324
--- /dev/null
+++ b/.planning/phases/17-docker-deployment/17-01-PLAN.md
@@ -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"
+---
+
+
+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
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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
+
+
+
+
+
+ Task 1: Enable standalone output, limit DB pool, remove @vercel/blob
+ teressa-copeland-homes/next.config.ts, teressa-copeland-homes/src/lib/db/index.ts, teressa-copeland-homes/package.json
+
+ - teressa-copeland-homes/next.config.ts
+ - teressa-copeland-homes/src/lib/db/index.ts
+ - teressa-copeland-homes/package.json
+
+
+ 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.
+
+
+ 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"
+
+
+ - `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
+
+ next.config.ts has standalone output, db pool is limited to 5, @vercel/blob removed from package.json
+
+
+
+ Task 2: Create /api/health endpoint with DB connectivity check
+ teressa-copeland-homes/src/app/api/health/route.ts
+
+ - teressa-copeland-homes/src/lib/db/index.ts
+ - teressa-copeland-homes/src/lib/db/schema.ts (first 20 lines, for import pattern)
+
+
+ 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.
+
+
+ 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"
+
+
+ - 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
+
+ GET /api/health endpoint exists, runs SELECT 1 via Drizzle, returns 200/503 based on DB reachability
+
+
+
+
+
+All code changes compile:
+```bash
+cd teressa-copeland-homes && npx tsc --noEmit
+```
+
+
+
+- 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
+
+
+
diff --git a/.planning/phases/17-docker-deployment/17-02-PLAN.md b/.planning/phases/17-docker-deployment/17-02-PLAN.md
new file mode 100644
index 0000000..26eb048
--- /dev/null
+++ b/.planning/phases/17-docker-deployment/17-02-PLAN.md
@@ -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'
+---
+
+
+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
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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
+
+
+
+
+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)
+
+
+
+
+
+
+ Task 1: Create Dockerfile, .dockerignore, and .env.production.example
+ teressa-copeland-homes/Dockerfile, teressa-copeland-homes/.dockerignore, teressa-copeland-homes/.env.production.example
+
+ - 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
+
+
+ **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).
+
+
+ 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"
+
+
+ - `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)
+
+ Dockerfile with three-stage build, .dockerignore, and .env.production.example all created with correct content
+
+
+
+ Task 2: Create docker-compose.yml and update .gitignore
+ teressa-copeland-homes/docker-compose.yml, teressa-copeland-homes/.gitignore
+
+ - teressa-copeland-homes/Dockerfile (just created in Task 1)
+ - teressa-copeland-homes/.gitignore
+
+
+ **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.
+
+
+ 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"
+
+
+ - `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
+
+ docker-compose.yml with env_file secrets, DNS fix, named volume, and restart policy; .gitignore updated
+
+
+
+
+
+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
+```
+
+
+
+- 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/
+
+
+