Move the browser off the VPS to its own unit (#46) #52
Reference in New Issue
Block a user
Delete Branch "feat/46-remote-browser"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #46 once deployed.
The headless browser leaves the API stack and becomes its own compose unit
(
chrome/docker-compose.yml) intended for the home machine, reached over thetailnet. No fallback sidecar is left on the VPS.
The backend needs no code change —
BROWSER_WS_URLwas already the onlycoupling. Its default is now empty rather than a pinned Docker IP, so an
unconfigured or unreachable browser degrades exactly as it always has: plain-TLS
libraries unaffected, kagane/novelfull logged and skipped, stored covers still
served.
What shipped
chrome/docker-compose.yml+chrome/.env.example— the browser unit, withthe CDP port bound to
${BROWSER_BIND_ADDR}(no default) and the resourcelimits from the epic: 512 MiB / 1 GiB memory+swap,
oom_score_adj 800,halved CPU weight, shm 1 GiB -> 128 MiB.
depends_onand thebrowsernetwork.bookmark-apigains thedefaultnetwork. Droppingbrowserhad left it ondbalone, which isinternal: true— no published port and, worse, noegress for the poller at all. Caught by actually bringing the stack up.
DEPLOY.md§7 for first-time setup of the browsermachine;
REDEPLOY.md§8 for its independent update cadence; architecturediagrams, config tables and troubleshooting rows across README/AGENTS/env.
Verified locally
HeadlessChrome,all limits applied as declared.
TestSmokeKaganeImagefetched56710 bytes of
image/webp,TestSmokeKaganeGetgot a 200 with a realchapter list. The challenge cleared under the reduced 128 MiB shm.
on the configured one.
/healthz200; egress confirmed present ondefaultand absent ondb.go test ./...,go vet,gofmtclean.Left to the operator
Provisioning the home machine, the Tailscale ACL, setting
BROWSER_WS_URLinproduction, and observing acceptance criteria 5-7 (covers with the machine off,
several days of zero OOM/restarts, VPS memory improvement).
DEPLOY.md§7 nowcarries the before/after
free -mreading those need.The headless browser leaves the API stack. It becomes its own compose unit (chrome/docker-compose.yml) deployed on the home machine and reached over the tailnet, returning 471 MiB of working set to a 1974 MiB VPS that has no swap. No fallback sidecar is left behind. The backend needs no code change: BROWSER_WS_URL was already the only coupling, so relocation is one environment variable. Its default is now empty rather than a pinned Docker IP — an unreachable or unconfigured browser degrades exactly as it always has, with plain-TLS libraries unaffected, kagane and novelfull logged and skipped, and stored covers still served. The browser unit publishes CDP on ${BROWSER_BIND_ADDR} with no default, because CDP authenticates nothing and the home machine has a real LAN: an unset value must fail the deploy rather than silently expose an endpoint that is remote code execution for anything that reaches it. Resource limits are sized against the measured 645 MiB untuned peak and the CI runner that already holds 1.2 GiB of that box. bookmark-api gains the default network. Dropping `browser` left it on `db` alone, which is internal: true — that meant no published port and, worse, no egress for the poller at all. Caught by bringing the stack up. Docs: ADR-0006 for the topology, DEPLOY.md §7 for first-time setup of the browser machine, REDEPLOY.md §8 for its independent update cadence, plus the architecture diagrams, config tables and troubleshooting rows.Compose merges `networks:` across override files rather than replacing them, so the prod override's claim that it must re-name every network was false — and the rationale built on it ("proxy carries the poller's egress") was false too. Verified against `docker compose config`: the API renders on db, default and proxy with only `proxy` named here. Egress comes from `default`, which is now the thing a maintainer must not tidy away. chrome/.env.example shipped BROWSER_BIND_ADDR=100.x.y.z as a live value, so `cp .env.example .env && docker compose up` failed with Docker rejecting an invalid IP instead of the guard message both troubleshooting tables promise. Commented out, so the promised message is what you actually get. DEPLOY §7 gains the two steps that were asserted but never instructed: a Tailscale ACL, without which "Tailscale identity is the access control" is aspirational and 9222 is open to every device on the tailnet; and a VPS `free -m` reading before and after, without which the memory this move reclaims cannot be shown. Also drops a change-narration comment and three restatements of measured facts that already have a canonical home.