# 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// ├─ flip `current` symlink (atomic) ├─ docker compose up -d ├─ curl http://127.0.0.1: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: ["127.0.0.1:8096:8096"]` — never `0.0.0.0`. - `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 bound to `127.0.0.1` only; 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 |