Preview/prod split on the NAS (Vercel-style) — p-eop=nightly, eop=main
Why¶
Mimic Vercel's preview/prod split on the self-hosted NAS: p-eop.theestablishers.com serves the
nightly branch (preview), eop.theestablishers.com serves main (prod). Follow-on to T-073.
Status (2026-06-21)¶
- ✅ Cloudflare (via CF API, token
CLOUDFLARE_API_TOKEN): addedp-eop.theestablishers.comto theroofkeeper-nastunnel ingress →http://localhost:3001, plus a proxied DNS CNAME. Preserved the existingnas.andeop.routes. Verified: p-eop serves the app (307 →/auth/signin, NextAuth host = p-eop). - ✅ Containers:
eop-app-previewruns onlocalhost:3001with--memory 600m; prodeop-appcapped at1100m. Neither can fry the NAS — a runaway gets its container OOM-killed, host survives. - ✅ RAM monitor:
ram-monitorcontainer logs host MemAvailable/SwapFree every 5 min to/docker/eop/ram-monitor.log. At idle both apps fit (~230 MB free, modest swap). - ⏳ Both currently run
:latest(= nightly), so prod and preview are the same code for now. - ⚠️ RAM is tight; if the monitor shows sustained pressure / swap-thrash, drop the preview (prod + host unaffected).
Decision log¶
Retrofit note (added 2026-06-21 by Accounting (Diagnostics)): the entry below was authored by the EOP Local Assistance agent, per its
Attestation (EOP Local Assistance):byline. The**Source:** <name> · <session URL>convention was added to AGENTS.md mid-2026-06-21 — after EOP Local Assistance had already opened this task — so its session URL is not recorded here. If EOP Local Assistance re-opens this task in a future session it can append its own Source line. Source (edit): Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
2026-06-21 — split set up (Cloudflare + capped preview + RAM monitor)¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.mdand scanned the board — no existing task covered the preview/prod split, so opened T-082 (follow-on to T-073); kept current as work landed. - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- What changed: added the
p-eoptunnel ingress + DNS; stood up a memory-capped preview container + a standing RAM monitor; capped prod. - Proposed by: the owner. Approved by: the owner.
- Rationale: the owner wants a Vercel-style preview/prod split self-hosted; the 1.9 GB NAS is tight, so proceed only behind memory caps (preview can't fry the NAS) + a monitor + a drop-if-it-suffers fallback.
- Evidence — owner, 2026-06-21 (verbatim):
"Is it actually possible that … the NAS mimics Vercel by having p-eop.theestablishers.com to run on the GitHub branch nightly while eop.theestablishers.com run on main?"
"if it wouldn't fry my NAS for possibly overloading it, I'd actually like you to proceed without the RAM upgrade, while have measure implemented to monitor the RAM usage just to make sure the performance suffice, or else we'll just drop the split"
Next (to make it a real nightly-vs-main split)¶
- Per-branch CI — build
nightly→eop-app:nightly,main→eop-app:main; preview pulls:nightly, prod pulls:main(auto-build on push). - Promote
nightly→main(owner OK required — a push tomain) soeop=main is current, else prod would revert ~104 commits. - Watch
/docker/eop/ram-monitor.log; drop the preview if it thrashes.
Note (2026-06-21) — p-eop Firebase auth error is NOT the split → see ticket I-002¶
- My earlier diagnosis here was wrong. p-eop's Firebase sign-in error ("Unable to save initial state /
sessionStorage") is not caused by the split: p-eop is already a Firebase authorized domain, the
/auth/signinresponse headers are byte-identical to eop's (so it isn't Cloudflare), and the sign-in code useswindow.location.origin(not the bakedNEXT_PUBLIC_APP_URL). It's a FirebasesignInWithRedirectcross-domainauthDomain+ browser storage-partitioning issue — tracked as ticketI-002(escalates to aT-NNNwhen fixed). The preview-image rebuild below is still worth doing for correctness, but it won't fix that auth error.
2026-06-22 — split is LIVE; preview is OOM-thrashing¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.md; tracking T-082, kept current. - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- Split live: the repoint completed on the NAS ~
2026-06-21T18:08(after a long pull stall that jammed the DSM channel for hours — only the channel result was lost; the docker recreate ran).eop-app → eop-app:main(eop-baked, restarts=0, stable) +eop-app-preview → eop-app:nightly(p-eop-baked). Both serve 307. Verified via the recovered DSM channel. - ⚠️ Preview thrashing:
eop-app-previewhas 11 OOM-restarts — the 600 MB cap is too low for the Next.js server (OOMs ~every 40 min). The cap contains it: prod (eop-app, 1100 MB) is unaffected, the host can't be fried. Degraded-but-safe preview, not a host risk. - Decision (owner, 2026-06-22): KEEP the preview running. Asked the owner keep / stop-but-configured / drop; they chose keep — the thrash is contained (prod + host safe), and p-eop stays available for T-047 / I-002 testing. Revisit only if the NAS gets a RAM upgrade (then raise the cap) or if the owner later wants it dropped.
DONE — 2026-06-22¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.md; closing T-082 with verdict + commits + blast radius per the "close the loop" rule. - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- Verdict — what landed vs. plan: the Vercel-style preview/prod split is live and current.
eop.theestablishers.comserves main (prod),p-eop.theestablishers.comserves nightly (preview), both on the NAS behind theroofkeeper-nasCloudflare tunnel. Per-branch CI bakes a distinct image per branch (NEXT_PUBLIC_APP_URLset per ref); both containers are memory-capped (prod 1100 MB, preview 600 MB) with a standing RAM monitor. Verified via the DSM channel:eop-appon:main(restarts=0, stable),eop-app-previewon:nightly(serving 307); both307 → /auth/signin. The preview self-OOMs (~11 restarts) but the cap contains it — prod + host unaffected; owner chose to keep it. - Commits:
4b13062d(per-branch CI —main→eop-app:main,nightly→eop-app:nightly);eop=main was made current by a fast-forward ref move ofmaintonightly's tip (owner-authorized push to main, not a new commit); decision-log + close-out docs in7c12369d+ this entry. The earlier setup (Cloudflare ingress/DNS, capped containers, RAM monitor) was applied via the CF API + the NAS, not git. - Blast radius for other agents:
.github/workflows/nas-image.ymlnow builds two images keyed by branch — anyone changing the deploy/CI path must keep both:mainand:nightlytags + the per-branchNEXT_PUBLIC_APP_URLbaking.mainis now ~current withnightly(was ~104 commits behind) — prod (eop) reflects nightly's state as of the promotion; futuremaindivergence is by design (promote nightly→main to ship).- NAS RAM is the binding constraint (1.9 GB): the preview runs hot. Any new always-on container on the NAS competes for the same headroom — coordinate before adding one.
- Not in scope here: p-eop's Firebase sign-in error is
I-002/T-083(same-originauthDomain), not the split. Testing auth flows on p-eop waits on T-083.
Post-close note — 2026-06-22 — branches kept in LOCKSTEP (owner directive)¶
- Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- The split infrastructure built here stays in place, but per an owner directive the branch routing is
unified for now:
mainandnightlyare kept current together (ship tomain) until further notice — the owner isn't usingp-eop. The live policy + the owner's verbatim instruction are in the 🚦 Operational signals banner at the top ofAGENTS.md. Blast radius: every push now reaches prod (eop); the preview rebuilds too (unused). Flip that signal to 🔴 to re-separate — no rebuild needed.
Future direction (NOT yet decided) — 2026-06-22 — prod→Vercel, nightly→NAS¶
- Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- The owner floated a contingency: move prod back to Vercel (managed / always-on) and run the nightly/preview on the NAS. Affirmed as sound — correct risk allocation (prod off a single 1.9 GiB box shared with home-automation; the NAS keeps the throwaway preview). The original reason for leaving Vercel (Hobby compute) was largely addressed by T-073's de-intensification + the 2-user base; remaining prep = deploy the pending Firestore index, leave crons on the NAS (T-084), bank-login already external.
- Coordination note for other agents: don't over-invest in hardening prod-on-NAS — prod's likely long-term home is Vercel again. Not actioned; kept in mind.
- Owner, 2026-06-22 (verbatim):
"worst case, we could have prod re-deployed completely to Vercel, while we run nightly on my NAS some day (if you affirm then I'll keep this in mind)"
2026-06-22 — preview DROPPED (NAS resource relief)¶
- Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- Owner: "Let's drop the preview image on my NAS." Done — verified no auto-recreate first (no watchtower, no
cron, no deploy script; the container was a manual
docker run), so it stays down: - Container
eop-app-previewstopped + removed; imageeop-app:nightlyremoved from the NAS. - CI
nas-image.ymltrigger is nowbranches: [main]—nightlyno longer auto-builds an image (manualworkflow_dispatchon the nightly ref still can). The nightly branch + the task-board build (docs-deploy.yml) are unaffected. - Cloudflare p-eop DNS CNAME deleted (verified gone;
eop+nasDNS confirmed intact). The tunnel ingress rule (p-eop → localhost:3001) was left dormant on purpose — harmless without DNS, ready if the preview is revived; safer than editing the shared tunnel config that also routes prod. - Result (verified): prod
eop-app(:main) still 307/healthy; 3001 refused; host RAM available 322 → 721 MiB (used 1401 → 1004 MiB). The OOM-restart churn is gone. - Blast radius:
p-eopis down by design. This closes I-002 (symptom can't recur without the domain) and downgrades T-083 to latent prod-auth hardening (low priority — no active symptom; prod popup sign-in is the working path). Branch policy unchanged (🟢 lockstep, ship tomain). Minor follow-up: thedocs/deploy/nas-runbook.mdpreview section is now stale.