docs(07-02): complete plan summary — Dockerfile and .env.example S3/R2 vars

This commit is contained in:
Arthur Belleville 2026-05-15 18:20:38 +02:00
parent 0781403f5c
commit 2329e19e75
No known key found for this signature in database

View file

@ -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