From 2329e19e757da4846f80f0ae9caeb387805fc924 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 15 May 2026 18:20:38 +0200 Subject: [PATCH] =?UTF-8?q?docs(07-02):=20complete=20plan=20summary=20?= =?UTF-8?q?=E2=80=94=20Dockerfile=20and=20.env.example=20S3/R2=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../phases/07-deploy-v1/07-02-SUMMARY.md | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .planning/phases/07-deploy-v1/07-02-SUMMARY.md diff --git a/.planning/phases/07-deploy-v1/07-02-SUMMARY.md b/.planning/phases/07-deploy-v1/07-02-SUMMARY.md new file mode 100644 index 0000000..5daa7fd --- /dev/null +++ b/.planning/phases/07-deploy-v1/07-02-SUMMARY.md @@ -0,0 +1,104 @@ +--- +phase: 07-deploy-v1 +plan: "02" +subsystem: backend-deploy +tags: [go, docker, dockerfile, multi-stage, distroless, s3, env] +dependency_graph: + requires: + - 07-01 (embed.FS anchor + RunMigrations — both binaries reference backend package) + provides: + - backend/Dockerfile — 3-stage multi-stage Docker build producing /app/web and /app/worker (DEPLOY-01) + - backend/.env.example — complete operator reference for S3/R2 and production vars (DEPLOY-02) + affects: + - backend/Dockerfile (new file) + - backend/.env.example (updated) +tech_stack: + added: + - gcr.io/distroless/static-debian12:nonroot — minimal runtime base image (D-07) + - node:20-alpine — asset compilation stage (Tailwind standalone CLI v4.0.0) + - golang:1.26-alpine — Go compilation stage + patterns: + - Multi-stage Docker build: assets → builder → runtime + - No CMD in final stage — docker-compose overrides command: per service (D-08) + - CGO_ENABLED=0 static binaries for distroless compatibility (D-07) + - templ generate at pinned v0.3.1020 before go build (gitignored *_templ.go files) + - go build cache mount (--mount=type=cache,target=/root/.cache/go-build) for layer caching +key_files: + created: + - backend/Dockerfile + modified: + - backend/.env.example +decisions: + - "node:20-alpine for assets stage — matches Node version used by JS monorepo apps/api/Dockerfile" + - "Tailwind downloaded as standalone binary (not npm package) — matches justfile bootstrap approach" + - "Both go build commands use separate RUN --mount=type=cache,target=/root/.cache/go-build — enables independent layer caching per binary" + - "DOMAIN var commented out in .env.example — it is only needed in production .env.prod and should not have a misleading default value" + - "S3_USE_PATH_STYLE=true as dev default — MinIO requires path-style; prod R2 operator must flip to false" +metrics: + duration: "~4 minutes" + completed: "2026-05-15" + tasks: 2 + files: 2 +--- + +# Phase 7 Plan 2: Multi-stage Dockerfile and .env.example S3/R2 vars Summary + +## What Was Built + +3-stage multi-stage Dockerfile producing two CGO_ENABLED=0 static binaries (/app/web and /app/worker) in a distroless nonroot runtime image, plus .env.example updated with S3/R2 vars, MAX_UPLOAD_SIZE_MB, and a commented DOMAIN entry for production operators. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Multi-stage Dockerfile for web + worker binaries | f29bf0c | backend/Dockerfile | +| 2 | Update .env.example with S3/R2, DOMAIN, MAX_UPLOAD_SIZE_MB vars | 0781403 | backend/.env.example | + +## Decisions Made + +1. The assets stage uses `node:20-alpine` (not pure alpine + curl) to avoid custom tool installation — the Tailwind standalone CLI is a binary download anyway, so any alpine works; node:20 matches the JS monorepo API Dockerfile for consistency. + +2. Both `go build` RUN instructions use separate `--mount=type=cache` directives so Docker can cache each binary's compilation independently. If only `cmd/web` changes, the `cmd/worker` layer reuses the cache. + +3. `DOMAIN` is commented out in `.env.example` rather than given a placeholder value. A live domain value with no comment context could mislead operators into thinking the empty string is a valid default; the comment makes the production-only context explicit. + +4. `S3_USE_PATH_STYLE=true` is the dev default (MinIO requires path-style URLs). The comment explicitly instructs production operators to set this to `false` for Cloudflare R2 virtual-hosted-style URLs. + +5. `TEST_DATABASE_URL` comment updated to flag it as dev/test only — the .env.example serves as the reference for `.env.prod` and the clarification prevents accidentally including test connection strings in production. + +## Deviations from Plan + +None — plan executed exactly as written. + +## Verification + +Dockerfile structure verified by inspecting key markers: +- 3 named stages: `assets`, `builder`, runtime (from `gcr.io/distroless/static-debian12:nonroot`) +- `templ generate` at `v0.3.1020` +- `CGO_ENABLED=0 GOOS=linux` on both go build commands +- No `CMD` instruction in final stage +- No `COPY .env*` anywhere (T-07-05 mitigated) + +.env.example verified: +``` +grep count for S3_ENDPOINT|S3_BUCKET|S3_ACCESS_KEY|S3_SECRET_KEY|S3_REGION|S3_USE_PATH_STYLE|MAX_UPLOAD_SIZE_MB|DOMAIN = 8 +``` +All 8 new vars present. All original vars (DATABASE_URL, TEST_DATABASE_URL, SESSION_SECRET, PORT, ENV) preserved. + +## Known Stubs + +None. + +## Threat Flags + +No new threat surface beyond plan's threat_model. + +- T-07-05 mitigated: No `.env*` files are COPY'd in any Dockerfile stage. Secrets reach the runtime only via `env_file:` in docker-compose at runtime. +- T-07-07 mitigated: `:nonroot` tag confirmed in final FROM line — runs as uid 65532. + +## Self-Check: PASSED + +- backend/Dockerfile: EXISTS +- backend/.env.example: EXISTS (contains S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY, S3_USE_PATH_STYLE, MAX_UPLOAD_SIZE_MB, DOMAIN) +- Commit f29bf0c: VERIFIED in git log +- Commit 0781403: VERIFIED in git log