oll.am · Deploy runbook · Scout stack · Prepared 2026-07-14 · Deploy = tomorrow morning

The scout stack goes live — console-only morning sequence.

Everything below is prepared and verified; nothing is deployed. Tomorrow morning: merge three gated PRs in order, click four Coolify apps into existence, run one corpus migration, mint two fresh secrets, smoke each hop. The recommended cut keeps the agent seam local for one release — one moving piece less.

Gates: #96 → #95 → #94 (all CI-green) New apps: ollama · oll-memory · ollscout New DB: Neon oll_memory (pgvector) Image verified: ollscout 939d407

1 · Merge gates per-PR go from Sam · green CI · in this order

Three PRs carry the stack. The order matters: #96 is the deploy vehicle (the oll-memory service the other two call), #95 layers the seam's scout verbs on top, #94 adds the scout agent service. All three were CI-green at prep time (2026-07-14). Per the house merge gate, each needs an explicit per-PR go — merging is itself a morning console act.

OrderPRBranchWhat it lands
1st#96feat/oll-memoryThe memory spine — pgvector RAG service (services/oll-memory), caller metadata, stable-id upsert. Everything else deploys against this.
2nd#95feat/oll-mcp-thinThe Agent Seam — thin MCP adapter + 7 job-scout verbs (contract v5, 185 tests). Stays a local runtime in this release (§7).
3rd#94feat/oll-scout-agenticThe agentic scout MVP — JSON-LD source + cited-rationale scorer + dedup (58 tests).
If a later PR shows conflicts after an earlier merge (squash-merge follow-up), rebase it on main before its go — do not merge through a red or stale check.

2 · The topology cut recommendation: minimal — memory + GUI up, seam + scout stay local

Two pieces of the stack are stateless callers (the seam, the OpenCLAW scout loop) and two are the things worth hosting (the corpus, the GUI). The minimal cut moves only the second pair to the VPS. The seam keeps running locally exactly as today — it just points its OLL_MEMORY_BASE at the deployed service instead of the laptop container. One moving piece less on deploy morning; the seam's containerization becomes a calm follow-up instead of a morning blocker (it has no Dockerfile yet — §10).

VPS · Coolify · network ollam — deploy tomorrow
ollama — nomic-embed-text, embeddings onlyinternal: http://ollama:11434
oll-memory — the corpus service, Neon-backedinternal: http://oll-memory:5008 · public: https://memory.oll.am
ollscout — the Dossier GUI (separate repo)public: https://scout.oll.am → memory over the ollam network
Laptop — unchanged this release
oll-mcp seam — launchd, --http :5097OLL_MEMORY_BASE flips → https://memory.oll.am
OpenCLAW scout loop — keeps calling the seamhost.docker.internal:5097 — no URL change needed
poc-pg / poc-oll-memory — retire after §9 smoke passeskeep 48h as rollback, then docker compose down

The follow-up (not tomorrow): Dockerfile for services/oll-mcp → Coolify app → OpenCLAW seam URL flips host.docker.internal:5097 → the deployed seam.

Why memory.oll.am must be public in this cut: the local seam and any laptop tooling reach the corpus over HTTPS. DNS already wildcards to the VPS, so binding the domain in Coolify gets Let's Encrypt TLS automatically. The service-token wall (X-Service-Token, fresh secret — §8) is the only door.

3 · Console: Neon DB + the Ollama embeddings app the two dependencies, first

3a — Neon: new database oll_memory

  1. Neon console → the existing oll.am project (house rule: new DB on the existing project, never a new project) → New database: oll_memory.
  2. Enable pgvector on it: CREATE EXTENSION IF NOT EXISTS vector; (the migration block in §6 includes this — the service's boot hook also attempts it, but don't rely on lazy creation for a prod deploy).
  3. Copy the pooled connection string — it becomes oll-memory's DATABASE_URL.

3b — Ollama: one small app, embeddings only

  1. Coolify → new app from image ollama/ollama, attach to network ollam, network alias ollama, persistent volume on /root/.ollama (models survive redeploys). No public domain — internal only.
  2. After it's healthy, pull the embedding model once (Coolify terminal or SSH): docker exec <ollama-container> ollama pull nomic-embed-text.
This Ollama serves embeddings only (nomic-embed-text, 768-dim, ~274 MB) — retrieval, not generation. Generation stays with the house default (Groq via oll-model). CPU inference is fine at this size; see §10 for the sizing note.

4 · Console: oll-memory the standard Base-Directory pattern

Coolify fieldValue
Sourcebytesbysamu/oll-am · branch main (after the §1 merges)
Base Directory/services/oll-memory
DockerfileDockerfile (in the base dir; non-root, HEALTHCHECK on /api/health)
Watch Pathsservices/oll-memory/**
Network / aliasollam · oll-memory
Port / domainPORT=5008 (set explicitly — Coolify's default injection is 3000) · https://memory.oll.am
Auto-deployOFF once healthy (frozen-once-working, like every backend app)

Environment

VarValueWhy
DATABASE_URLpostgresql://…neon…/oll_memoryIts own private DB (database-per-service). Boot-gated: missing = refuses to start.
OLL_MEMORY_SERVICE_TOKEN<fresh secret — §8>NOT poc-token. Every caller sends it as X-Service-Token.
EMBED_PROVIDERollamaReal semantic embeddings. This makes OLLAMA_EMBED_BASE_URL boot-required — fail-loud by design.
OLLAMA_EMBED_BASE_URLhttp://ollama:11434The §3b app over the internal ollam network — no public hop.
EXTRACT_PROVIDERmock (default — omit)The scout stack only queries/ingests; real extraction (needs OLL_MODEL_* env) can be enabled later without a rebuild.
PORT5008Keeps the internal address oll-memory:5008 true for every caller.

5 · Console: ollscout separate repo — the Dossier GUI at scout.oll.am

ollscout lives in its own repo (bytesbysamu/ollscout, like ollwrite). The production Dockerfile is committed and verified (939d407): Next.js 15 standalone, multi-stage node:22-alpine, non-root, HEALTHCHECK on 127.0.0.1:$PORT/, 12-factor PORT (default 3000). Build + run were proven locally against the real corpus — HTTP 200 with live rail content, container health green.

The loopback bind was dev-only. package.json's dev/start scripts bind 127.0.0.1 — that never applies in the container: standalone node server.js honors HOSTNAME=0.0.0.0 baked into the image. Nothing to configure.
Coolify fieldValue
Sourcebytesbysamu/ollscout · branch main
Base Directory / Dockerfile/ · Dockerfile (repo root)
Networkollam (to reach oll-memory internally)
Port / domain3000 (image default) · https://scout.oll.am (DNS already wildcards → TLS auto)

Environment (the contract is documented in the Dockerfile itself)

VarValueNote
OLL_MEMORY_BASEhttp://oll-memory:5008Internal ollam hop — the GUI never leaves the VPS to read the corpus.
OLL_MEMORY_SERVICE_TOKEN<same fresh secret as §4>Server-side only; never reaches the browser.
SCOUT_USER_ID2Single-user instance by design (§10) — reads mem:user:2:*.
OLL_WRITE_BASEhttps://write.oll.amLive write spine (default baked in — set anyway, explicit beats implicit).
OLL_WRITE_JWT<fresh 72h Core JWT — §8>Powers the draft/rewrite buttons. Expires in 72h — see §10.
SCOUT_USER_NAMESam’s signer nameStamped on generated letters; optional.

6 · Terminal: corpus migration local poc-pg → Neon · recommended: dump/restore

Recommendation: pg_dump/pg_restore. The dump carries the embeddings (768-dim vectors, same data_memory_chunk table both sides), all 58 stored cases, and — the part re-ingest cannot reproduce — the accumulated rulings history (statuses, verdict notes, tick register). Zero re-embedding, minutes of work. Run everything through the poc-pg-1 container so client/server versions can't disagree:

# 0) the Neon URL for the new DB — export without echoing it anywhere
export NEON_MEM_URL='postgresql://…neon…/oll_memory?sslmode=require'

# 1) dump the local corpus (custom format, portable)
docker exec poc-pg-1 pg_dump -U mem -d oll_memory -Fc --no-owner --no-privileges -f /tmp/oll_memory.dump

# 2) pgvector first — the restore fails without the extension
docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'psql "$URL" -c "CREATE EXTENSION IF NOT EXISTS vector;"'

# 3) restore into Neon
docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'pg_restore -d "$URL" --no-owner --no-privileges /tmp/oll_memory.dump'

# 4) the retrieval knob that does NOT travel with a single-DB dump —
#    re-apply it or prod retrieval silently degrades vs. the audited local setup
docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'psql "$URL" -c "ALTER DATABASE oll_memory SET hnsw.ef_search = 80;"'

# 5) sanity — chunk count should match the local side
docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'psql "$URL" -tc "SELECT count(*) FROM data_memory_chunk;"'
docker exec poc-pg-1 psql -U mem -d oll_memory -tc "SELECT count(*) FROM data_memory_chunk;"
The alternative — re-ingest from scratch: re-ingest the CV (the poc ingest script) and let the scout rebuild the jobs corpus over its normal ticks. Tradeoff: clean corpus and a true end-to-end test of the prod ingest path, but it loses every ruling, score and verdict note accumulated across 58 cases, and the jobs only refill as sources re-surface them (days, not minutes). Use it only if the restore misbehaves — the corpus is rebuildable, the case history is not.

7 · Re-point the local seam the only OpenCLAW-side change in this cut

In the minimal cut the seam and OpenCLAW don't move — OpenCLAW keeps calling host.docker.internal:5097 exactly as today. The single change: the seam's environment (launchd plist / deploy/run-http.sh env) flips its memory base to the deployed service, with the fresh token:

OLL_MEMORY_BASE=https://memory.oll.am
OLL_MEMORY_SERVICE_TOKEN=<the fresh §8 secret>   # poc-token dies with the poc stack

Restart the launchd job; the seam's OLL_MCP_MATCH_FLOOR needs no env at all anymore — the measured 0.64 floor is the baked default since the retrieval audit, so a restart without the var can no longer loosen the gate.

Follow-up (explicitly not tomorrow): give services/oll-mcp a Dockerfile (it has none — its deploy artifacts are a launchd plist + run-http.sh, a desktop-run design), deploy it as a Coolify app with OLL_MEMORY_BASE=http://oll-memory:5008 internal, and flip OpenCLAW's seam URL from host.docker.internal:5097 to the deployed seam. Then nothing scout-critical runs on the laptop.

8 · Fresh secrets mint on deploy morning — nothing dev-grade goes to prod

SecretMintGoes to
OLL_MEMORY_SERVICE_TOKENopenssl rand -hex 32oll-memory app (§4) · ollscout app (§5) · local seam env (§7). The dev poc-token is burned — it was committed in the poc compose; it must never guard the prod corpus.
OLL_WRITE_JWTcd services/oll-mcp && AUTH_JWT_SECRET=<Core's, from Coolify env> .venv/bin/oll-mcp mint-jwt --user 2ollscout app (§5). Identity-only 72h HS256 — the same setup-token pattern ollwrite uses. 72h expiry caveat in §10.
OLL_MCP_JWTsame mint — or nothingLocal seam. Preferred: nothingrun-http.sh already re-mints from OLL_MCP_ENV_FILE (AUTH_JWT_SECRET) on every start, so the daily launchd restart keeps it fresh automatically.
Handling rule: mint in the terminal, paste into Coolify env fields, never into a file in any repo and never into chat/logs. AUTH_JWT_SECRET is read from Coolify's Core app env (or core/.env locally) — it is the one secret that is reused, not minted, because the JWT must verify against the live Core.

9 · Smoke verification per service, in dependency order — green before the next

9a — ollama

docker exec <ollama-container> ollama list        # nomic-embed-text present

9b — oll-memory (liveness, then an authed query against the migrated corpus)

curl -s https://memory.oll.am/api/health
# → {"service":"oll-memory","status":"ok"}

curl -s -X POST https://memory.oll.am/api/memory/query \
  -H "X-Service-Token: $NEW_TOKEN" -H "Content-Type: application/json" \
  -d '{"query":"software engineer","collection":"mem:user:2:jobs","acl":["u:2"],"top_k":3}'
# → non-empty results[] with real job chunks + vector scores
#   (this one call proves the whole chain: token wall → Neon → Ollama embed → retrieval)

9c — ollscout

9d — the re-pointed local seam

launchctl kickstart -k gui/$(id -u)/am.oll.mcp-seam    # restart with the new env
# then one ollam_memory_search / ollam_job_get through OpenCLAW or the MCP Inspector
# → cited chunks from the MIGRATED corpus (same case ids as before the move)
Retire the poc stack only after 9a–9d are green: keep poc-pg-1/poc-oll-memory-1 down-but-not-deleted for 48h as the rollback copy, then docker compose -f scripts/poc/docker-compose.scout-memory.yml down -v.

10 · Honest deltas & risks what this runbook does not hand-wave

Delta / riskRealityMitigation
scout.oll.am is public, the data is personalThe GUI has no login — it renders one person's CV analysis, applications and rulings to anyone who finds the URL. Fine on a laptop, not on the open internet.Gate it at the edge before or at deploy: Coolify/Traefik basic-auth middleware on the domain (a console field, no code). A real Core-JWT login is the product-ization follow-up.
Single-user by designSCOUT_USER_ID=2 is baked per instance; there is no tenancy in the GUI. This is a personal deployment, not the multi-user oll-scout service yet.Accepted for this release; the multi-user model rides the Core-JWT login follow-up.
Ollama on the VPS — sizingnomic-embed-text is small (~274 MB, 768-dim) and embeddings are cheap on CPU, but it's a new always-on container on a VPS already running ~15 containers; first pull needs ~1 GB free disk and inference wants ~1 GB RAM headroom.Check docker stats/df -h before creating the app. Embeddings-only — never point generation at it.
72h JWT expiry (OLL_WRITE_JWT)ollscout's draft buttons hold a static 72h token — they die silently ~3 days after deploy. The seam self-heals (re-mint on restart); the GUI does not.Accept for launch; re-mint via Coolify env edit + restart when drafts 401. Follow-up: the same re-mint-on-boot pattern, or a long-lived service token on oll-write.
ef_search does not travelALTER DATABASE … SET hnsw.ef_search=80 is per-database config; a single-DB pg_restore does not carry it. Forgetting it = silently worse recall than the audited local setup.Step §6.4 — it's in the migration block, not a footnote.
Re-ingest cost (if dump/restore fails)Re-embedding ~58 cases × 2–4 chunks on VPS CPU is minutes and free — but the rulings/verdict history is unrecoverable by re-ingest.That asymmetry is why dump/restore is the recommendation and re-ingest only the fallback.
oll-mcp has no DockerfileThe seam was built as a desktop runtime (launchd + run-http.sh); it cannot be a Coolify app today.Exactly why the minimal cut keeps it local. Containerizing it is the named follow-up (§7), not a hidden assumption.
Coolify PORT injectionCoolify defaults injected PORT to 3000; oll-memory's callers assume oll-memory:5008.§4 sets PORT=5008 explicitly — the same class of bug that 502'd Core at C0.

Related pages: oll-scout service deploy runbook (the #94 service itself, 2026-07-11) · the Memory-spine decision · the full scout review.