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.
83 lines
4.3 KiB
Markdown
83 lines
4.3 KiB
Markdown
# Stack, hosts, ports, budgets
|
||
|
||
## Hosts (measured 2026-10-17)
|
||
|
||
| Host | Name | Role | RAM total / free | Swap used | CPUs |
|
||
|---|---|---|---|---|---|
|
||
| `.13` | `nixos-desktop` | **Runtime** — app, Qdrant, Prefect worker | 15 GiB / **2.3 GiB** | **8.1 GiB** | 12 |
|
||
| `.35` | `sam-Ubuntu1` | **Public edge** — Caddy terminates 443; also Gitea, Immich, Nextcloud, Penpot, Wiki.js | 14 GiB / 4.1 GiB | 4.0 GiB | 2 |
|
||
| `.27` | `sam-4screen-desktop` | **Build / dev** — sources, Obsidian vault, agent work | — | — | — |
|
||
|
||
`.13` is memory-bound, not CPU-bound. The budget is real: exceeding it destabilises services Sam relies on.
|
||
|
||
## Ports
|
||
|
||
| Service | Host | Port | State |
|
||
|---|---|---|---|
|
||
| Caddy | `.35` | 443 (+ LAN 8000) | existing — **Sam applies any change** |
|
||
| **Go app** | `.13` | **8096** | new — verify free (`8095` = offsite.lab); published on `192.168.20.13:8096`, **never** `0.0.0.0` |
|
||
| Qdrant REST | `.13` | 6333 | new, `127.0.0.1` only |
|
||
| Qdrant gRPC | `.13` | 6334 | new, `127.0.0.1` only |
|
||
| Prefect server | `.13` | existing | reuse — do not deploy a second |
|
||
| OmniRoute | `.13` | existing | reuse — the only model gateway |
|
||
| Postgres | `.13` | 5432 | reuse if needed |
|
||
| Whisper / Piper | `.13` | existing | reuse (Phase 7 voice only) |
|
||
| LangGraph service | `.13` | 8096+1 or existing | Phase 5 — see note below |
|
||
|
||
**Known occupied:** 3000, 3020, 5000, 5002, 5432, 8000, 8001, 8080, 8082, 8090, 8091, 8092, 8095, 8230, 8231, 8500, 8501, 8788–8792, 3081–3084, 1883.
|
||
|
||
## Memory budget
|
||
|
||
| Component | Cap |
|
||
|---|---|
|
||
| Go app | 120 m |
|
||
| Qdrant | 400 m |
|
||
| Prefect worker | 150 m |
|
||
| Guardrail (if local) | 300 m |
|
||
| **Total ceiling** | **1.0 GiB / 4 containers** |
|
||
|
||
### Qdrant memory, measured (2026-10-26, v1.20.0, 400 m cap, 1024 dims)
|
||
|
||
| Chunks | Settled RSS | Peak during ingest |
|
||
|---|---|---|
|
||
| 500 | 128 MiB | 149 MiB |
|
||
| 5,000 | 138 MiB | 305 MiB |
|
||
| 20,000 | 151 MiB | 292 MiB |
|
||
|
||
Within the cap at all three sizes, but only with the memory-frugal collection
|
||
settings in `corpus/qdrant.py`: HNSW graph and payloads on disk
|
||
(`hnsw_config.on_disk`, `on_disk_payload`), `m=8`, `ef_construct=64`,
|
||
`memmap_threshold=5000`, `indexing_threshold=20000`. With Qdrant's defaults the
|
||
container is OOM-killed between 5,000 and 20,000 chunks. The pipeline is the only
|
||
code that creates the collection, so those settings cannot drift.
|
||
|
||
Ceiling: 20,000 chunks at 1024 dimensions. Beyond that, set
|
||
`RESUME_QDRANT_VECTORS_ON_DISK=1`, reduce the dimension, or raise the cap — in
|
||
that order. Detail: `docs/phase-4-notes.md` §7.
|
||
|
||
Every service sets `mem_limit` **and** `memswap_limit` to the same value so it cannot consume host swap. Bind services to `127.0.0.1`; Caddy on `.35` is the only public listener.
|
||
|
||
**One documented amendment (Go app, port 8096):** the app publishes on the LAN address `192.168.20.13:8096`, not `127.0.0.1`. Caddy runs on `.35` and proxies to `192.168.20.13:8096`, so a loopback-only publish is unreachable from the edge and the public URL returns 502. This is the same way `8095` (offsite), `8092` (photo-filter), `3909` (garage-ui) and `4200` (prefect) are published on `.13`. The port is LAN-only — never `0.0.0.0` — and Caddy on `.35` is still the only public listener, so `AGENTS.md` §4.2 holds in substance. Decision: Sam, 2026-10-26, interaction `54aeb1a2`. `deploy/deploy.sh` follows the same address (`RESUME_PUBLISH_IP`, default `192.168.20.13`) in its health gate and shape assertion.
|
||
|
||
## Paths
|
||
|
||
| Item | Path |
|
||
|---|---|
|
||
| Repo working copy | `/home/sam/paseo/projects/resume_ai_system_paperclip_build` |
|
||
| Release root | `/home/sam/deployment/resume/` |
|
||
| Live symlink | `/home/sam/deployment/resume/current` |
|
||
| Secrets | `~/.config/environment.d/10-secrets.conf` |
|
||
| Planning reference | `/home/sam/paseo/projects/resume_ai_system/plan/` (read-only) |
|
||
|
||
## Public entry
|
||
|
||
`resume.lab.audasmedia.com.au` — wildcard DNS already resolves to the public edge on `.35`. **No DNS work is required.** Caddy block is specified in Phase 3 §3.3.
|
||
|
||
## Open items
|
||
|
||
| Item | Owner |
|
||
|---|---|
|
||
| Embedding provider (Gemini free tier vs OpenRouter vs local) | Sam — Phase 4 shipped provider-agnostic (`corpus/embeddings.py`); the `omniroute` adapter is ready, `local-hashing` is a placeholder, so the decision is still Sam's before Phase 5 answers questions |
|
||
| Repo public/private after launch | Sam — decide after Phase 3 |
|
||
| Phone number on the contact page | Sam |
|