chore: merge executor worktree (worktree-agent-ad2dff45f7520558c)
This commit is contained in:
commit
45701bf8aa
3 changed files with 234 additions and 0 deletions
104
.planning/phases/07-deploy-v1/07-02-SUMMARY.md
Normal file
104
.planning/phases/07-deploy-v1/07-02-SUMMARY.md
Normal 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
|
||||
|
|
@ -4,6 +4,7 @@ DATABASE_URL=postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable
|
|||
# Postgres connection string used by integration tests (auth, session, etc.).
|
||||
# Falls back to DATABASE_URL if unset; tests skip if neither is set.
|
||||
# The test harness creates an isolated schema per test run and drops it on cleanup.
|
||||
# DEV/TEST ONLY — do NOT include TEST_DATABASE_URL in .env.prod (not used at runtime).
|
||||
TEST_DATABASE_URL=postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable
|
||||
|
||||
# Session secret — 32 random bytes hex-encoded. Used as the CSRF authentication key.
|
||||
|
|
@ -16,3 +17,39 @@ PORT=8080
|
|||
|
||||
# Environment selector: "development" enables the slog text handler; "production" switches to JSON.
|
||||
ENV=development
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# S3-compatible object storage (Cloudflare R2 in production, MinIO in dev)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# S3 endpoint URL.
|
||||
# Dev (MinIO): http://localhost:9000
|
||||
# Production (R2): https://<account-id>.r2.cloudflarestorage.com (D-06)
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
|
||||
# S3 bucket name.
|
||||
S3_BUCKET=xtablo-dev
|
||||
|
||||
# S3 region. Cloudflare R2 accepts "auto" or a standard region token; "us-east-1" is safe default.
|
||||
S3_REGION=us-east-1
|
||||
|
||||
# S3 access key (MinIO dev default: minioadmin).
|
||||
S3_ACCESS_KEY=minioadmin
|
||||
|
||||
# S3 secret key (MinIO dev default: minioadmin).
|
||||
S3_SECRET_KEY=minioadmin
|
||||
|
||||
# Use path-style S3 URLs.
|
||||
# true — for MinIO and other self-hosted S3 (path-style: http://host/bucket/key).
|
||||
# false — for Cloudflare R2 (virtual-hosted-style: https://bucket.host/key).
|
||||
S3_USE_PATH_STYLE=true
|
||||
|
||||
# Maximum file upload size in megabytes. Default 25 if unset.
|
||||
MAX_UPLOAD_SIZE_MB=25
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Production domain (used in docker-compose.prod.yaml for Caddy TLS — D-04)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Uncomment and set to your domain in .env.prod.
|
||||
# DOMAIN=app.yourdomain.com
|
||||
|
|
|
|||
93
backend/Dockerfile
Normal file
93
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Multi-stage Dockerfile for the Xtablo Go backend.
|
||||
#
|
||||
# Three stages:
|
||||
# assets — downloads Tailwind CLI, HTMX, and Sortable.js then compiles the
|
||||
# Tailwind output CSS from tailwind.input.css + templates/. (D-09)
|
||||
# builder — copies assets, runs `templ generate`, and compiles both binaries
|
||||
# with CGO_ENABLED=0 so they are fully static. (D-07)
|
||||
# runtime — copies only the two binaries into a minimal distroless image.
|
||||
# No CMD is set — docker-compose overrides command: per service. (D-08)
|
||||
#
|
||||
# Build context: backend/ directory (sibling to go.mod).
|
||||
# From the repo root: docker build -f backend/Dockerfile backend/
|
||||
# From inside backend/: docker build .
|
||||
#
|
||||
# Security: no .env files are ever COPY'd into any layer (T-07-05).
|
||||
# The :nonroot distroless tag runs as uid 65532, preventing filesystem writes (T-07-07).
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 1: assets
|
||||
# Downloads pinned versions of Tailwind CLI, HTMX, and Sortable.js,
|
||||
# then compiles tailwind.input.css against templates/ for the minified CSS.
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:20-alpine AS assets
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
RUN mkdir -p static
|
||||
|
||||
# Tailwind standalone CLI — pinned at v4.0.0 (matches justfile tailwind_version)
|
||||
RUN curl -sSL -o /usr/local/bin/tailwindcss \
|
||||
"https://github.com/tailwindlabs/tailwindcss/releases/download/v4.0.0/tailwindcss-linux-x64" \
|
||||
&& chmod +x /usr/local/bin/tailwindcss
|
||||
|
||||
# HTMX — pinned at major version 2 (matches justfile htmx_version)
|
||||
RUN curl -sSL -o static/htmx.min.js \
|
||||
"https://unpkg.com/htmx.org@2/dist/htmx.min.js"
|
||||
|
||||
# Sortable.js — pinned at v1.15.7 (matches justfile sortable_version)
|
||||
RUN curl -sSL -o static/sortable.min.js \
|
||||
"https://cdn.jsdelivr.net/npm/sortablejs@1.15.7/Sortable.min.js"
|
||||
|
||||
# Copy Tailwind input and templates for content-scanning
|
||||
COPY tailwind.input.css .
|
||||
COPY templates/ templates/
|
||||
|
||||
# Compile and minify Tailwind CSS
|
||||
RUN tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 2: builder
|
||||
# Compiles both Go binaries (cmd/web and cmd/worker) with CGO_ENABLED=0.
|
||||
# Runs `templ generate` first since *_templ.go files are gitignored. (D-07)
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Download dependencies first (layer-cached until go.mod/go.sum change)
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy the entire backend context
|
||||
COPY . .
|
||||
|
||||
# Overwrite static/ with the freshly built assets from the assets stage
|
||||
COPY --from=assets /build/static ./static
|
||||
|
||||
# Install templ at the pinned version and run template generation
|
||||
# (gitignored *_templ.go files must be generated at build time)
|
||||
RUN go install github.com/a-h/templ/cmd/templ@v0.3.1020 && templ generate
|
||||
|
||||
# Compile the web server binary — CGO_ENABLED=0 required for distroless/static
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o /app/web ./cmd/web
|
||||
|
||||
# Compile the background worker binary — CGO_ENABLED=0 required for distroless/static
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o /app/worker ./cmd/worker
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 3: runtime
|
||||
# Minimal distroless image containing only the two compiled binaries.
|
||||
# Runs as nonroot (uid 65532) — no filesystem write access. (T-07-07)
|
||||
# No CMD set — docker-compose.prod.yaml provides `command:` per service. (D-08)
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
COPY --from=builder /app/web /app/web
|
||||
COPY --from=builder /app/worker /app/worker
|
||||
|
||||
EXPOSE 8080
|
||||
Loading…
Reference in a new issue