Phase 3: release script, container limits, rehearsed rollback

Adds the deployment mechanism for the portfolio site. The site itself is not
deployed yet: Phase 2 has not produced a binary.

- deploy/deploy.sh: build + test (fail fast) -> releases/<ts> -> atomic
  `current` symlink flip -> docker compose up -d -> /healthz gate ->
  flip back to `previous` and exit non-zero on failure. `--rollback`
  restores the previous release in one command. Auto-detects the Go
  toolchain in /nix/store when `go` is not on PATH.
- deploy/docker-compose.yml: 127.0.0.1:8096, mem_limit == memswap_limit ==
  120m, capped json-file logs (10m x 3), restart unless-stopped.
- deploy/Dockerfile: runtime image only (alpine + prebuilt payload), so a
  release needs no toolchain, no network and no package install.
- deploy/rehearse.sh: repeatable proof (21 assertions, exit 0) of deploy,
  build fail-fast, health-gate rollback, --rollback and N3 restart.
- deploy/README.md + docs/phase-3-evidence.md: how it works, the build
  contract Phase 2 must satisfy, the Caddy block for Sam, and the open
  bind-address question (loopback-only vs reachable from .35).

Satisfies DoD items 1, 3 (container), 4, 8. Items 2, 5, 6, 7 need the Phase 2
binary and Sam's Caddy change; recorded as deferred with owners, not claimed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
System
2026-10-26 17:31:13 +11:00
parent b267e55f8c
commit 8b3209fab5
6 changed files with 880 additions and 0 deletions

28
deploy/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# resume — runtime image (Phase 3 §3.2).
#
# Built by deploy/deploy.sh from a release directory, with the prebuilt static
# Go binary already in place. No Go toolchain, no network and no package install
# at build time: the image is `alpine` + the release payload, nothing else.
#
# The binary must be statically linked (deploy.sh's fallback build sets
# CGO_ENABLED=0). If the app ever needs cgo, change the base image to a glibc
# one (e.g. golang:1.25-bookworm) in git — do not patch it on the host.
FROM alpine:3.21
ARG APP_VERSION=dev
LABEL org.opencontainers.image.title="resume" \
org.opencontainers.image.version="${APP_VERSION}" \
org.opencontainers.image.description="Sam Rolfe — portfolio site (Mode A static)"
WORKDIR /srv
# bin/server plus whatever the build emitted (dist/ for rendered pages/assets).
COPY . /srv/
ENV PORT=8096 \
APP_VERSION=${APP_VERSION}
EXPOSE 8096
ENTRYPOINT ["/srv/bin/server"]