Once the port is published on 192.168.20.13 the host no longer listens on 127.0.0.1:8096, so wait_healthy(), do_status() and the rollback path would all get connection-refused and every deploy would roll back. They now probe $PUBLISH_IP (RESUME_PUBLISH_IP, default 192.168.20.13). assert_shape() asserts the published host IP is that address instead of loopback; the mem_limit == memswap_limit checks are unchanged. check_port stays address-agnostic on `sport = :8096`. Records the amendment in reference/stack.md (ports table + the bind rule) and docs/phase-3-deploy.md (§3.2 bullet, flow diagram, DoD line), citing Sam's decision of 2026-10-26, interaction 54aeb1a2. AGENTS.md is Sam's to change and is untouched.
108 lines
4.1 KiB
Markdown
108 lines
4.1 KiB
Markdown
# Phase 3 — Public deployment
|
|
|
|
**Goal:** a live URL Sam can send someone. Early, deliberately — not at the end.
|
|
|
|
**Depends on:** Phase 2.
|
|
|
|
> **Read `AGENTS.md` §4.2 first.** This phase touches a shared host. The Caddy change is **Sam's to apply**, not the agent's.
|
|
|
|
---
|
|
|
|
## Facts
|
|
|
|
| Item | Value |
|
|
|---|---|
|
|
| Public hostname | `resume.lab.audasmedia.com.au` |
|
|
| DNS | Wildcard `*.lab.audasmedia.com.au` already resolves publicly — **no DNS work needed** |
|
|
| TLS | Terminated by Caddy on `.35` |
|
|
| App port | `8096` on `.13` (verify free; 8095 belongs to `offsite.lab`) |
|
|
| Release path | `/home/sam/deployment/resume/` |
|
|
|
|
---
|
|
|
|
## Work
|
|
|
|
### 3.1 The release mechanism
|
|
|
|
```
|
|
deploy.sh
|
|
├─ build + test (fail fast)
|
|
├─ copy to releases/<timestamp>/
|
|
├─ flip `current` symlink (atomic)
|
|
├─ docker compose up -d
|
|
├─ curl http://192.168.20.13:8096/healthz → must be ready
|
|
└─ on failure: flip back to `previous`, exit non-zero
|
|
```
|
|
|
|
- `deploy.sh --rollback` restores the previous release in one command.
|
|
- No agent hand-edits files on a production host. Agents invoke the script or fix the script in git.
|
|
- Deploy is a **script**, not a model decision. `AGENTS.md` §4.6.
|
|
|
|
### 3.2 Container
|
|
- `mem_limit` = `memswap_limit` = `120m`.
|
|
- `ports: ["192.168.20.13:8096:8096"]` — **never** `0.0.0.0`.
|
|
Caddy runs on `.35`, so the port has to be reachable from the LAN; a
|
|
`127.0.0.1` publish answers only on `.13` and the public URL returns 502.
|
|
LAN-only publish, Caddy on `.35` still the sole public listener. Decision:
|
|
Sam, 2026-10-26, interaction `54aeb1a2`. The health gate and `assert_shape()`
|
|
in `deploy.sh` follow the same address (`RESUME_PUBLISH_IP`).
|
|
- `logging` capped (`max-size: 10m`, `max-file: 3`).
|
|
- `restart: unless-stopped`.
|
|
|
|
### 3.3 Caddy — **produce the diff, Sam applies it**
|
|
|
|
Agent output is the exact block to add to `.35`'s Caddyfile, plus the reload command. Do not apply it.
|
|
|
|
```caddy
|
|
resume.lab.audasmedia.com.au {
|
|
encode zstd gzip
|
|
reverse_proxy 192.168.20.13:8096
|
|
header {
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
X-Content-Type-Options "nosniff"
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
X-Frame-Options "SAMEORIGIN"
|
|
Content-Security-Policy "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'"
|
|
}
|
|
@static path *.css *.js *.svg *.png *.jpg *.webp *.woff2
|
|
header @static Cache-Control "public, max-age=31536000, immutable"
|
|
}
|
|
```
|
|
|
|
Apply with `caddy reload` (not restart). Verify the other hostnames still resolve afterwards.
|
|
|
|
### 3.4 Repo visibility
|
|
Repo stays private through this phase. Sam decides on public visibility after the URL is verified.
|
|
|
|
---
|
|
|
|
## Definition of Done
|
|
|
|
- [ ] `deploy.sh` and `deploy.sh --rollback` both work; a deliberate bad build proves rollback.
|
|
- [ ] `https://resume.lab.audasmedia.com.au` serves the site.
|
|
- [ ] Container published on `192.168.20.13:8096` only, reachable from `.35`; confirmed with `ss -ltn`.
|
|
- [ ] `memswap_limit == mem_limit`; confirmed with `docker inspect`.
|
|
- [ ] Security headers present; CSP does not break the page.
|
|
- [ ] A1 still passes over the public URL with the AI layer stopped.
|
|
- [ ] Sam has applied the Caddy block and confirmed the other lab hostnames still work.
|
|
- [ ] Rollback rehearsed once and documented.
|
|
|
|
---
|
|
|
|
## Out of scope
|
|
|
|
- Rate limiting for the AI endpoint (Phase 6).
|
|
- Monitoring, alerting, restore drills — add a minimal uptime check only.
|
|
- Any change to other sites, containers or the NixOS config.
|
|
|
|
---
|
|
|
|
## Risks
|
|
|
|
| Risk | Mitigation |
|
|
|---|---|
|
|
| Caddy change breaks another site | Additive block only; `caddy reload` not restart; Sam verifies the other hostnames immediately after |
|
|
| Deploying a half-finished build | `deploy.sh` health gate + rollback; the site is designed to be useful at every stage |
|
|
| Port 8096 occupied | Verify before compose; pick the next free port and record it in `reference/stack.md` |
|
|
| Host memory pressure | 120 m cap; the app is static-serving and should idle well under it. Measure and record actual RSS |
|