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:
93
deploy/README.md
Normal file
93
deploy/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Deployment (Phase 3)
|
||||
|
||||
One script, one release directory, one atomic symlink. Deploy is a script, not a
|
||||
model decision (`AGENTS.md` §4.6), and nobody hand-edits files on the runtime
|
||||
host: change these files in git and deploy again.
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `deploy.sh` | build + test → release → flip → `up -d` → health gate → rollback on failure |
|
||||
| `deploy.sh --rollback` | restore the previous release in one command |
|
||||
| `docker-compose.yml` | runtime container: `127.0.0.1:8096`, `mem_limit == memswap_limit == 120m`, capped logs |
|
||||
| `Dockerfile` | runtime image only — alpine + the prebuilt release payload, no toolchain, no network |
|
||||
| `rehearse.sh` | repeatable proof that deploy, fail-fast, health-gate rollback and `--rollback` all work |
|
||||
|
||||
## Layout on `.13`
|
||||
|
||||
```
|
||||
/home/sam/deployment/resume/
|
||||
├── releases/<UTC timestamp>/ immutable: bin/server, dist/, compose, Dockerfile, manifest.txt, .env
|
||||
├── current -> releases/<ts> live release; swapped with ln + mv -T (atomic rename)
|
||||
├── previous -> releases/<ts> the release that was live before the last flip
|
||||
└── deploy.log append-only record of every attempt, including failures
|
||||
```
|
||||
|
||||
## What `deploy.sh` expects from the app
|
||||
|
||||
1. `make build` if a `Makefile` with a `build` target exists, otherwise
|
||||
`go build ./cmd/server`. Either way the result must be one server binary at
|
||||
`bin/server` or `dist/server`; anything else the binary serves from disk goes
|
||||
in `dist/` and is copied into the release.
|
||||
2. `make test` (or `go test ./...`) — runs **before** anything is copied or
|
||||
flipped, so a broken build never reaches the release directory.
|
||||
3. `GET /healthz` → `200`. If the body is JSON with a `static` field it must be
|
||||
`true`. AI readiness is deliberately **not** part of the gate: Mode A must
|
||||
work with every AI service stopped (acceptance test A1).
|
||||
4. Bind `$PORT` (default `8096`) on `0.0.0.0` **inside the container**. The
|
||||
container's own loopback is not reachable through a published port; exposure
|
||||
is controlled by the host side of `ports:` in `docker-compose.yml`.
|
||||
|
||||
## Deploying
|
||||
|
||||
```bash
|
||||
cd /home/sam/paseo/projects/resume_ai_system_paperclip_build
|
||||
./deploy/deploy.sh # build, test, release, flip, health gate
|
||||
./deploy/deploy.sh --rollback # one command back to the previous release
|
||||
tail -40 /home/sam/deployment/resume/deploy.log
|
||||
```
|
||||
|
||||
Exit code is the answer: `0` means the new release answered `/healthz`.
|
||||
Non-zero means either (a) build/test failed and **nothing changed**, or (b) the
|
||||
health gate failed, `current` was flipped back to `previous`, the previous
|
||||
release was restarted, and the log says which.
|
||||
|
||||
Flags worth knowing: `--source DIR` (build from another checkout — this is how
|
||||
`rehearse.sh` drives the script), `--root DIR`, `--port N`,
|
||||
`--health-timeout N`.
|
||||
|
||||
The Go toolchain is auto-detected, including `/nix/store/*-go-1.*/bin/go`, so
|
||||
the script works on a host where `go` is not on `PATH`.
|
||||
|
||||
## Rehearsing the rollback
|
||||
|
||||
```bash
|
||||
./deploy/rehearse.sh # 21 assertions, exit 0 when every one holds
|
||||
```
|
||||
|
||||
It builds three throwaway services in a scratch directory, runs the real script
|
||||
against the real release root and the real port, and asserts:
|
||||
|
||||
1. a good release deploys, answers `/healthz`, publishes on `127.0.0.1` only,
|
||||
and has `memswap_limit == mem_limit`;
|
||||
2. a build failure exits non-zero, leaves `current` untouched and keeps serving;
|
||||
3. a release whose `/healthz` is `500` is rolled back by the health gate, exits
|
||||
non-zero, and the previous release serves again;
|
||||
4. `deploy.sh --rollback` flips back to the previous release and exits 0;
|
||||
5. `docker compose down && up` (acceptance test N3) restores the service.
|
||||
|
||||
It removes the release root, container and images it created. `KEEP_REHEARSAL=1`
|
||||
keeps them for inspection.
|
||||
|
||||
## Container limits — not negotiable
|
||||
|
||||
`mem_limit` and `memswap_limit` are both `120m`, so the container cannot eat
|
||||
host swap on a host that is already swapping. Measured RSS for a Go static
|
||||
server is single-digit MiB; `deploy.sh` prints the real figure after every
|
||||
deploy so drift shows up in the log.
|
||||
|
||||
## Caddy
|
||||
|
||||
`docs/phase-3-deploy.md` §3.3 holds the block for `.35`. It is additive, applied
|
||||
with `caddy reload` (never a restart), and **Sam applies it** — see
|
||||
`docs/phase-3-evidence.md` for the open bind-address question that decides
|
||||
whether that block points at `192.168.20.13:8096` or at `.13`'s Caddy.
|
||||
Reference in New Issue
Block a user