---
phase: 05-deploy
plan: "01"
subsystem: container-infra
tags: [docker, fly, litestream, dumb-init, entrypoint, drizzle-migrate]
dependency_graph:
  requires:
    - "apps/server/litestream.yml (Plan 03 — must exist before image build)"
    - "apps/server/scripts/run-migrations.ts (Plan 12 — compiled to dist/scripts/run-migrations.js)"
    - "apps/server/src/otel-init.ts (Plan 08 — compiled to dist/otel-init.js)"
  provides:
    - "apps/server/Dockerfile — multi-stage image build definition"
    - "apps/server/docker-entrypoint.sh — 6-step boot sequence with restore+migrate+replicate"
    - "apps/server/.dockerignore — build-context exclusions (legacy/, .planning/, decomp/, localList.txt)"
  affects:
    - "Plan 04 (lint-deploy-stack.mjs will grep these files for pin assertions)"
    - "Plan 05 (deploy-staging.yml CI exercises image build end-to-end)"
    - "Plan 14 (pnpm verify:phase-5 composite wires lint-deploy-stack)"
tech_stack:
  added:
    - "node:22-bookworm-slim (builder + runtime base)"
    - "litestream/litestream:0.3.13 (binary-copy stage)"
    - "dumb-init (APT — PID 1 zombie reaping + SIGTERM forwarding)"
  patterns:
    - "Multi-stage Dockerfile: builder → litestream → runtime"
    - "ENTRYPOINT + exec $@ pattern for correct PID 1 SIGTERM delivery"
    - "Pre-migrate snapshot → migrate → replicate sequence (D-09)"
key_files:
  created:
    - apps/server/Dockerfile
    - apps/server/docker-entrypoint.sh
    - apps/server/.dockerignore
  modified: []
decisions:
  - "Used node:22-bookworm-slim (glibc) for both builder and runtime — avoids Alpine native-module build failures for argon2 + better-sqlite3 (RESEARCH §Pitfall 1)"
  - "Litestream pinned to 0.3.13 tag — conservative pin per RESEARCH §State-of-the-Art; v2 upgrade path documented in RESTORE.md (Plan 13)"
  - "CMD passes --import ./dist/otel-init.js so Plan 08 OTel init loads before server code"
  - "pnpm deploy --prod --legacy /tmp/server-prod prunes devDeps + hoists native .node files into self-contained bundle"
  - "DB_PATH derived from DATABASE_URL env (strips no sqlite: scheme; raw path used with Litestream)"
metrics:
  duration: "~10 minutes"
  completed: "2026-05-08"
  tasks_completed: 2
  tasks_total: 2
  files_created: 3
  files_modified: 0
---

# Phase 05 Plan 01: Multi-stage Dockerfile + Container Entrypoint Summary

**One-liner:** 3-stage Dockerfile (bookworm-slim builder, litestream:0.3.13 binary, bookworm-slim runtime) with 6-step entrypoint (cold-restore → pre-migrate snapshot → drizzle migrate → litestream replicate background → SIGTERM trap → exec node).

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Multi-stage Dockerfile + .dockerignore | 8ebd641 | apps/server/Dockerfile, apps/server/.dockerignore |
| 2 | docker-entrypoint.sh | ed5b70f | apps/server/docker-entrypoint.sh |

## Dockerfile Stage Names and Versions

| Stage | Base Image | Purpose |
|-------|-----------|---------|
| `builder` | `node:22-bookworm-slim` | Install build-essential + python3, pnpm install --frozen-lockfile, compile all workspace packages, pnpm deploy --prod --legacy |
| `litestream` | `litestream/litestream:0.3.13` | Source of /usr/local/bin/litestream binary only |
| `runtime` | `node:22-bookworm-slim` | Slim image with ca-certificates + dumb-init; runs as USER node |

## Entrypoint Step Order (matches D-09)

1. Export `LITESTREAM_ACCESS_KEY_ID` / `LITESTREAM_SECRET_ACCESS_KEY` from Tigris-injected `AWS_*` vars
2. **Cold restore** — `litestream restore -if-replica-exists -o "$DB_PATH" "$DB_PATH"` (only if DB missing)
3. **Pre-migrate snapshot** — `cp "$DB_PATH" "/data/snapshots/pre-migrate-$(date +%s).db"` (D-09 safety net)
4. **Run migrations** — `node dist/scripts/run-migrations.js` (BLOCKING; `set -e` → crashloop on failure)
5. **Start replication** — `litestream replicate -config /etc/litestream.yml &` + capture `LITESTREAM_PID`
6. **Signal trap** — `trap 'kill -TERM $LITESTREAM_PID ...' TERM INT` (Pitfall 3 mitigation)
7. **Exec node** — `exec "$@"` (Pitfall 2 mitigation; dumb-init PID 1 delivers SIGTERM to Node)

## Deviations from Plan

None — plan executed exactly as written. Both RESEARCH §Pattern 1 and §Pattern 2 are followed verbatim with the specified refinements (otel-init CMD, chown, mkdir /data, --import flag).

## Cross-Plan Notes

- **Plan 03** must produce `apps/server/litestream.yml` — COPY'd into image at `/etc/litestream.yml`. Image build fails until this file exists.
- **Plan 12** must produce `apps/server/scripts/run-migrations.ts` — compiled output lands at `dist/scripts/run-migrations.js`. Entrypoint Step 4 calls this path; container crashloops until it exists.
- **Plan 08** must produce `apps/server/src/otel-init.ts` — compiled output lands at `dist/otel-init.js`. CMD `--import ./dist/otel-init.js` requires this file; server exits with MODULE_NOT_FOUND until Plan 08 is complete.
- **Plan 04** (`lint-deploy-stack.mjs`) will re-assert `node:22-bookworm-slim`, `litestream/litestream:0.3.13`, `dumb-init`, and `.dockerignore` exclusions as drift guards.
- **Plan 05** (`deploy-staging.yml` CI) exercises the full image build end-to-end.

## Threat Model Coverage

All STRIDE threats from plan threat_model are mitigated:

| Threat | Mitigation | Location |
|--------|-----------|---------|
| T-DEP-01 (info disclosure via image layers) | .dockerignore excludes legacy/, .planning/, decomp/, **/localList.txt | apps/server/.dockerignore |
| T-DEP-07 (supply-chain tampering) | Pinned node:22-bookworm-slim + litestream:0.3.13 | Dockerfile lines 5, 26, 29 |
| T-DEP-08 (SIGTERM swallowed by shell) | dumb-init PID 1 + exec "$@" in entrypoint | docker-entrypoint.sh:46 |
| T-DEP-Litestream (replicate dies silently) | trap sends SIGTERM to LITESTREAM_PID before shell exit | docker-entrypoint.sh:42 |
| T-DEP-Drizzle (bad migration corrupts data) | Pre-migrate snapshot + set -e crashloop | docker-entrypoint.sh:27-34 |
| T-DEP-Privesc (container privilege escalation) | USER node before WORKDIR in Dockerfile | Dockerfile:48 |

## Known Stubs

None. These are infrastructure files with no UI rendering or placeholder data.

## Self-Check

- [x] apps/server/Dockerfile exists
- [x] apps/server/docker-entrypoint.sh exists
- [x] apps/server/.dockerignore exists
- [x] Task 1 commit 8ebd641 exists
- [x] Task 2 commit ed5b70f exists
- [x] Verify commands both exit 0
- [x] [doc->REQ-DEP-01] present in Dockerfile (line 1) and entrypoint (line 2)
- [x] [doc->REQ-DEP-03] present in entrypoint (line 2)

## Self-Check: PASSED
