xtablo-source/.planning/phases/07-deploy-v1/07-VERIFICATION.md
2026-05-15 18:33:02 +02:00

15 KiB

phase verified status score overrides_applied human_verification
07-deploy-v1 2026-05-15T00:00:00Z human_needed 5/5 must-haves verified 0
test expected why_human
Build the Docker image: cd backend && docker build -f Dockerfile -t xtablo-test . All three stages complete successfully — assets stage downloads Tailwind/HTMX/Sortable and compiles CSS; builder stage runs templ generate and compiles both /app/web and /app/worker with CGO_ENABLED=0; runtime stage copies binaries into distroless image. Docker Engine is not installed on this development machine. The Dockerfile structure was verified by static inspection but the actual multi-stage build cannot be executed here.
test expected why_human
Validate compose syntax: cd backend && docker compose -f docker-compose.prod.yaml config --quiet Exit 0 with no errors — all services, volumes, and environment variable references are syntactically valid. Docker Engine is not installed on this development machine. YAML structure was verified by static inspection.
test expected why_human
End-to-end deploy smoke test on a VM: follow the README First-time setup steps with a test domain and verify /healthz and /readyz return expected JSON. curl https://<domain>/healthz returns {"status":"ok"}; curl https://<domain>/readyz returns {"status":"ok","db":"ok"} after stack starts. Actual VM deployment and Caddy TLS provisioning cannot be tested programmatically without network infrastructure.

Phase 7: Deploy v1 Verification Report

Phase Goal: Deliver a single-container deployment: Dockerfile, compose stack, Caddyfile, and operational runbook so the Go+HTMX server can be deployed to a VPS with one command. Verified: 2026-05-15 Status: human_needed Re-verification: No — initial verification

Goal Achievement

Observable Truths

# Truth Status Evidence
1 D-07: A single Dockerfile builds both /app/web and /app/worker binaries in one image ✓ VERIFIED backend/Dockerfile exists with 3 named stages (assets, builder, runtime). Builder stage has two CGO_ENABLED=0 GOOS=linux go build commands targeting ./cmd/web (-o /app/web) and ./cmd/worker (-o /app/worker). Runtime stage copies both.
2 D-12: GET /healthz returns 200 with no DB connection (pure liveness) ✓ VERIFIED HealthzHandler() in handlers.go takes no args, returns 200+{"status":"ok"} immediately. Registered at r.Get("/healthz", HealthzHandler()) in router.go. TestHealthz_OK passes (0.00s).
3 D-13: GET /readyz returns 200 when DB is reachable and 503 when DB is down ✓ VERIFIED ReadyzHandler(pinger Pinger) uses 2s timeout, pings DB. Returns 503+{"status":"degraded","db":"down"} on error, 200+{"status":"ok","db":"ok"} on success. Registered at r.Get("/readyz", ReadyzHandler(pinger)). TestReadyz_OK and TestReadyz_Down both pass.
4 D-09: Static assets served from embedded FS, not disk ✓ VERIFIED backend/embed.go declares //go:embed all:staticvar Static embed.FS. main.go passes assets.Static to web.NewRouter. router.go uses fs.Sub(staticFS, "static") + http.FileServer(http.FS(sub)).
5 D-10: goose.Up() called at startup before router construction using embedded migrations ✓ VERIFIED backend/internal/db/migrate.go exports RunMigrations using goose.SetBaseFS + goose.Up. main.go line 74: db.RunMigrations(ctx, pool, assets.Migrations) called after pool creation, before q := sqlc.New(pool) and before web.NewRouter(...).
6 D-01/D-02/D-03: docker-compose.prod.yaml defines postgres, web, worker, caddy services ✓ VERIFIED File exists with all 4 services. postgres (internal DB, D-03), web (command: /app/web), worker (command: /app/worker), caddy (TLS terminator, ports 80/443/443udp).
7 Postgres service has no ports directive (internal-only) ✓ VERIFIED No ports: key on postgres service. Only expose: ["8080"] on web service (internal). Caddy is the only service with host port bindings.
8 D-04: Caddy service mounts deploy/Caddyfile with persistent caddy_data and caddy_config volumes ✓ VERIFIED ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro volume mount. Named volumes caddy_data:/data and caddy_config:/config declared in volumes block.
9 backend/deploy/Caddyfile uses {$DOMAIN} env var interpolation ✓ VERIFIED Caddyfile contains {$DOMAIN} { reverse_proxy web:8080 }.
10 D-11: backend/README.md documents deploy, rollback, and incident triage sections ✓ VERIFIED ## Deploy (line 241), ## Rollback (line 403), ## Incident Runbook (line 463) all present. First-time setup includes chmod 600 .env.prod and openssl rand -hex 32 for SESSION_SECRET. Rollback section documents TAG update + docker compose up -d.
11 Full Go test suite passes: go test ./... ✓ VERIFIED All packages pass: backend/internal/auth, backend/internal/db, backend/internal/files, backend/internal/jobs, backend/internal/web, backend/internal/web/ui, backend/templates. Exit 0.
12 go build ./cmd/web/... succeeds (binary compiles with embedded assets) ✓ VERIFIED go build ./cmd/web/... exits 0 with no output (no compilation errors).

Score: 5/5 ROADMAP success criteria verified (all 12 derived truths verified)

Required Artifacts

Artifact Expected Status Details
backend/embed.go //go:embed declarations for static/ and migrations/ ✓ VERIFIED Package assets. //go:embed all:staticvar Static embed.FS. //go:embed migrationsvar Migrations embed.FS.
backend/internal/db/migrate.go RunMigrations function using pgx/v5/stdlib bridge and goose.Up() ✓ VERIFIED Exports RunMigrations(ctx, pool, migrationsFS). Uses goose.SetBaseFS, goose.SetDialect("postgres"), goose.Up(sqlDB, "migrations").
backend/internal/web/handlers.go split HealthzHandler (no pinger) and new ReadyzHandler (pinger) ✓ VERIFIED HealthzHandler() http.HandlerFunc (no args). ReadyzHandler(pinger Pinger) http.HandlerFunc with 2s timeout.
backend/internal/web/router.go NewRouter with fs.FS param, /readyz route, /healthz liveness route ✓ VERIFIED Signature uses staticFS fs.FS. Routes: r.Get("/healthz", HealthzHandler()) and r.Get("/readyz", ReadyzHandler(pinger)).
backend/cmd/web/main.go Calls RunMigrations and passes assets.Static to NewRouter ✓ VERIFIED Line 74: db.RunMigrations(ctx, pool, assets.Migrations). Line 125: web.NewRouter(pool, assets.Static, ...).
backend/Dockerfile 3-stage multi-stage Docker build: assets, builder, runtime ✓ VERIFIED Stage assets (node:20-alpine): downloads Tailwind v4.0.0, HTMX@2, Sortable.js@1.15.7, compiles CSS. Stage builder (golang:1.26-alpine): runs templ@v0.3.1020 generate, builds both binaries with CGO_ENABLED=0 GOOS=linux. Stage runtime (gcr.io/distroless/static-debian12:nonroot): copies /app/web and /app/worker. No CMD. EXPOSE 8080.
backend/.env.example Documented env vars including S3/R2, DOMAIN, MAX_UPLOAD_SIZE_MB ✓ VERIFIED All 8 new vars present (S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY, S3_USE_PATH_STYLE, MAX_UPLOAD_SIZE_MB, DOMAIN). All original vars preserved.
backend/docker-compose.prod.yaml Production compose stack: postgres + web + worker + caddy ✓ VERIFIED All 4 services defined. postgres has no ports, web/worker use env_file: .env.prod + depends_on: postgres: condition: service_healthy. caddy has persistent named volumes.
backend/deploy/Caddyfile Caddy reverse proxy config with TLS and {$DOMAIN} interpolation ✓ VERIFIED {$DOMAIN} { reverse_proxy web:8080 }. LE staging note included.
backend/README.md Extended runbook with Deploy, Rollback, Incident sections ✓ VERIFIED Three H2 sections: ## Deploy (line 241), ## Rollback (line 403), ## Incident Runbook (line 463). All required subsections present.
From To Via Status Details
backend/cmd/web/main.go backend/internal/db/migrate.go db.RunMigrations(ctx, pool, assets.Migrations) ✓ WIRED Line 74 in main.go, confirmed by grep.
backend/cmd/web/main.go backend/embed.go assets.Static passed to web.NewRouter ✓ WIRED import assets "backend" (line 22). assets.Static used at line 125.
backend/internal/web/router.go ReadyzHandler r.Get("/readyz", ReadyzHandler(pinger)) ✓ WIRED Line 121 in router.go.
Dockerfile builder stage templ generate go install templ@v0.3.1020 && templ generate ✓ WIRED Line 72 in Dockerfile. Pinned at v0.3.1020.
Dockerfile builder stage /app/web and /app/worker binaries CGO_ENABLED=0 go build -o /app/web ./cmd/web ✓ WIRED Lines 75-80 in Dockerfile. Both commands use CGO_ENABLED=0 GOOS=linux.
docker-compose.prod.yaml web service Dockerfile /app/web binary command: /app/web with image: ${IMAGE}:${TAG} ✓ WIRED Lines 43, 42 in docker-compose.prod.yaml.
caddy service deploy/Caddyfile ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro ✓ WIRED Line 76 in docker-compose.prod.yaml.
Caddyfile web:8080 reverse_proxy web:8080 ✓ WIRED Line 25 in deploy/Caddyfile.

Behavioral Spot-Checks

Behavior Command Result Status
go build ./cmd/web/... exits 0 cd backend && go build ./cmd/web/... exit 0, no output ✓ PASS
TestHealthz_OK passes (no pinger arg) go test ./internal/web/... -run TestHealthz_OK -v PASS (0.00s) ✓ PASS
TestReadyz_OK passes (200, db:ok) go test ./internal/web/... -run TestReadyz_OK -v PASS (0.00s) ✓ PASS
TestReadyz_Down passes (503, db:down) go test ./internal/web/... -run TestReadyz_Down -v PASS (0.00s) ✓ PASS
Full test suite passes cd backend && go test ./... -count=1 All packages ok, exit 0 ✓ PASS

Probe Execution

No probes defined for this phase. Phase 7 produces infrastructure config files (Dockerfile, compose, Caddyfile) and Go code changes — probes are deferred to manual deployment verification (see Human Verification Required below).

Requirements Coverage

Requirement Source Plan Description Status Evidence
DEPLOY-01 07-02 Both binaries build into a single multi-stage Docker image ✓ SATISFIED backend/Dockerfile with 3 stages. Both /app/web and /app/worker compiled in builder stage, copied to runtime stage.
DEPLOY-02 07-02, 07-03 Image runs on single VPS with env-injected config ✓ SATISFIED backend/.env.example complete. docker-compose.prod.yaml uses env_file: .env.prod for web/worker. README documents VM deploy workflow.
DEPLOY-03 07-01, 07-03 Migrations run on deploy without manual intervention ✓ SATISFIED db.RunMigrations(ctx, pool, assets.Migrations) in cmd/web/main.go startup, before router construction. goose.Up() is idempotent.
DEPLOY-04 07-01 /healthz and /readyz return appropriate status codes ✓ SATISFIED /healthz → 200 always (liveness). /readyz → 200+db:ok or 503+db:down (readiness with DB probe). Tests pass.
DEPLOY-05 07-03 backend/README.md covers local dev, deploy, rollback, and incident triage ✓ SATISFIED README has ## Deploy (first-time + routine), ## Rollback (tag redeployment + break-glass schema rollback), ## Incident Runbook (/readyz 503, Caddy TLS, log viewing, distroless debug).

No orphaned requirements. All 5 DEPLOY-* requirements are claimed by plans and evidence found.

Anti-Patterns Found

No debt markers (TBD, FIXME, XXX, TODO, HACK, PLACEHOLDER) found in any files modified by this phase. No stub return patterns found. No placeholder implementations.

File Line Pattern Severity Impact
None found

Human Verification Required

Three items require human verification involving Docker Engine and network infrastructure:

1. Full Docker multi-stage build

Test: From inside backend/, run:

docker build -f Dockerfile -t xtablo-test . && echo "BUILD OK"
docker build -f Dockerfile --target builder -t xtablo-builder . && echo "BUILDER OK"

Expected: Both commands exit 0. The assets stage downloads Tailwind/HTMX/Sortable and compiles CSS. The builder stage runs templ generate and compiles both binaries with CGO_ENABLED=0. The final image contains only /app/web and /app/worker with no CMD. Why human: Docker Engine is not installed on this development machine. The Dockerfile structure passes static analysis but the actual build pipeline (including external CDN downloads of Tailwind, HTMX, Sortable) cannot be executed here.

2. docker compose config syntax validation

Test: From inside backend/, run:

docker compose -f docker-compose.prod.yaml config --quiet && echo "COMPOSE CONFIG OK"

Expected: Exit 0 — Docker validates all service definitions, volume declarations, environment variable references, and dependency conditions. Why human: Docker Engine is not available in this environment.

3. End-to-end production smoke test

Test: Follow the README First-time setup steps on a Hetzner VM (or equivalent host with a real domain):

  1. Copy backend/ to VM
  2. Create .env.prod with real values (DOMAIN, S3 credentials, POSTGRES_PASSWORD, SESSION_SECRET)
  3. docker build -f Dockerfile -t ghcr.io/yourusername/xtablo:v0.1.0 .
  4. export IMAGE=ghcr.io/yourusername/xtablo TAG=v0.1.0
  5. docker compose -f docker-compose.prod.yaml --env-file .env.prod up -d
  6. Verify: curl https://<domain>/healthz and curl https://<domain>/readyz

Expected:

  • Stack starts: postgres becomes healthy first, then web and worker start
  • goose.Up() runs migrations automatically at web startup (check docker compose logs web)
  • curl https://<domain>/healthz{"status":"ok"}
  • curl https://<domain>/readyz{"status":"ok","db":"ok"}
  • Caddy obtains TLS certificate from Let's Encrypt (use staging endpoint to avoid rate limits on first test) Why human: Actual VM, DNS, and Let's Encrypt provisioning cannot be tested programmatically.

Gaps Summary

No gaps found. All must-haves are verified in the codebase. All 5 DEPLOY requirements have implementation evidence. The phase goal — "single-container deployment: Dockerfile, compose stack, Caddyfile, and operational runbook deployable with one command" — is achieved in the codebase.

Three items require human verification (Docker build, compose syntax validation, and a live deploy smoke test) before treating this phase as fully closed for production use.


Verified: 2026-05-15 Verifier: Claude (gsd-verifier)