Closes the silent-deploy-rot gap caught by Phase 3n's triage. The
problem: a missing Gitea webhook left 11 commits stuck on an old
container while /healthz kept reporting 200 from the stale binary. With
no commit-level evidence on the wire, "deploy rolled" was unverifiable.
Mechanism:
- Dockerfile installs git, reads `git rev-parse --short HEAD` at build
time, injects via `-ldflags="-X main.gitCommit=<sha>"`. Works under
Dokploy's `git clone --depth 1` flow (the .git/ folder is in the
build context) and under plain `docker build .` (same). Local
`go run` falls back to "unknown".
- main.gitCommit assigns to web.Server.Version in main().
- /healthz now emits two lines: "ok" and "version: <sha>". Endpoint
remains unauthenticated so any worker / monitor can verify "deploy
rolled" without a session.
CLAUDE.md gets a mandatory "Post-deploy verification" section: after
every push, compare `git rev-parse --short HEAD` against
`curl /healthz | tail -1`. Mismatch = webhook broken; inspect Gitea
hook 172 (URL pattern `http://mlake.horse-ayu.ts.net:3000/api/deploy/
<refreshToken>` per the working webhooks on m/msbls.de + m/flexsiebels.de).
TestHealthzSurfacesVersion regression-guards the new line. Existing
TestHealthz updated to accept the multi-line body.
- Multi-stage Dockerfile: golang:1.25-alpine builder → distroless static
runtime as nonroot. Image weighs ~15 MB. Embeds templates, static
assets and migrations into the single binary.
- deploy/dokploy.yaml documents the Dokploy app for projax.msbls.de:
Tailscale-only, healthz path, single replica, secret PROJAX_DB_URL.
Translates to the Dokploy UI; not auto-applied.
- README rewritten as runbook: env vars, route table, test command,
deploy notes, trust model (Tailscale + no auth in v1, defer to
Supabase auth if it ever outgrows the fence), schema summary.
- .dockerignore strips .git, .m, .claude, docs, tests from build ctx.
- .gitignore covers ad-hoc binary and dist artefacts.
Verified locally: docker build succeeds, container responds to /healthz
and / against msupabase via --network host.