2a3bb6922d
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 the tailnet. No fallback sidecar is left on the VPS. The backend needs no code change — `BROWSER_WS_URL` was already the only coupling. 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, with the CDP port bound to `${BROWSER_BIND_ADDR}` (no default) and the resource limits from the epic: 512 MiB / 1 GiB memory+swap, `oom_score_adj 800`, halved CPU weight, shm 1 GiB -> 128 MiB. - API stack drops the service, its `depends_on` and the `browser` network. - `bookmark-api` gains the `default` network. Dropping `browser` had left it on `db` alone, which is `internal: true` — no published port and, worse, no egress for the poller at all. Caught by actually bringing the stack up. - ADR-0006 for the topology; `DEPLOY.md` §7 for first-time setup of the browser machine; `REDEPLOY.md` §8 for its independent update cadence; architecture diagrams, config tables and troubleshooting rows across README/AGENTS/env. ### Verified locally - Browser unit builds and runs: Chrome 151, UA carries no `HeadlessChrome`, all limits applied as declared. - **Live smoke passes through the new unit**: `TestSmokeKaganeImage` fetched 56710 bytes of `image/webp`, `TestSmokeKaganeGet` got a 200 with a real chapter list. The challenge cleared under the reduced 128 MiB shm. - Bind isolation proven: refused on the host's non-loopback address, accepted on the configured one. - 321 MiB peak of the 512 MiB cap after a full solve; 0 restarts, no OOM kill. - API stack comes up clean, `/healthz` 200; egress confirmed present on `default` and absent on `db`. - `go test ./...`, `go vet`, `gofmt` clean. ### Left to the operator Provisioning the home machine, the Tailscale ACL, setting `BROWSER_WS_URL` in production, and observing acceptance criteria 5-7 (covers with the machine off, several days of zero OOM/restarts, VPS memory improvement). `DEPLOY.md` §7 now carries the before/after `free -m` reading those need. Reviewed-on: #52 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
52 lines
2.6 KiB
Markdown
52 lines
2.6 KiB
Markdown
# ADR-0005: On-demand browser sidecar
|
|
|
|
Date: 2026-08-09
|
|
Status: accepted
|
|
|
|
Superseded in part by ADR-0006: the lifecycle below is unchanged, but the
|
|
service no longer lives in the API stack and the name `headless-shell` is gone.
|
|
|
|
## Decision
|
|
|
|
Keep the `headless-shell` service and its CDP port alive, but start Google Chrome
|
|
only when the first CDP connection arrives. The entrypoint supervises a `socat`
|
|
front-end, serializes browser start/reap state with `flock`, and tracks each
|
|
connection with a marker named for its helper PID. A reaper stops Chrome after
|
|
300 seconds with no live markers. Marker reconciliation covers a helper killed
|
|
before its cleanup trap runs.
|
|
|
|
Chrome runs in its own process group so reap sends the termination signal to
|
|
Chrome and its renderer children. The explicit `/home/chrome/profile` user-data
|
|
directory remains: Chrome remaps remote debugging to loopback on modern builds,
|
|
and Chrome ignores remote-debugging flags on a default profile. `socat` therefore
|
|
continues to front Chrome's loopback CDP port.
|
|
|
|
The profile is a named Compose volume. Clearance cookies survive both a reap and
|
|
`docker compose up --build`; the browser still starts with a fresh debugger UUID,
|
|
so chromedp must keep endpoint discovery enabled and must not use
|
|
`chromedp.NoModifyURL`.
|
|
|
|
The socat front-end and explicit profile are retained because Chromium remaps a
|
|
non-loopback debugging address to loopback since M113, while Chrome ignores the
|
|
remote-debugging flags on a default profile since Chrome 136. Flag tuning is
|
|
deliberately not adopted: its roughly 30% idle-footprint saving is irrelevant
|
|
to a browser that exists for seconds per wake and risks an untested fingerprint.
|
|
|
|
## Constraints
|
|
|
|
The 300-second floor is deliberate. Chromium batches cookie persistence on a
|
|
roughly 31-second timer, and Go's default HTTP transport can keep the discovery
|
|
connection parked for about 90 seconds after use. Reaping only with zero live
|
|
connections holds Chrome through both windows and through the poller's staggered
|
|
batch plus cover prefetch.
|
|
|
|
The anti-bot properties remain unchanged: a plausible non-UTC timezone, a
|
|
Chrome-version-derived User-Agent without `HeadlessChrome`, and no automation
|
|
flag. A remote browser restart can surface as `context.Canceled`, the same error
|
|
as a caller deadline, so the backend wraps cancellation observed with a closed
|
|
CDP connection as `browser interrupted`; the focused test asserts that
|
|
classification without killing a real browser.
|
|
The same process-group stop runs during supervisor shutdown, not only during
|
|
idle reap, so Chrome can flush its cookie batch before a container rebuild or
|
|
graceful stop.
|