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>
This commit was merged in pull request #52.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# The browser, as its own deployable unit.
|
||||
#
|
||||
# This does NOT run beside the API. It runs on the home machine, reached from
|
||||
# the VPS over the tailnet, and is updated without touching the API stack:
|
||||
#
|
||||
# cd chrome && docker compose up -d --build
|
||||
#
|
||||
# Set BROWSER_BIND_ADDR in chrome/.env to this machine's tailnet IP. See
|
||||
# ../DEPLOY.md §7 for the full first-time procedure and ../docs/adr/
|
||||
# 0006-browser-on-the-home-machine.md for why the browser lives here at all.
|
||||
name: bookmark-browser
|
||||
|
||||
services:
|
||||
browser:
|
||||
build: .
|
||||
image: bookmarkmanager-chrome:latest
|
||||
container_name: bookmark-browser
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Any real zone works, but a UTC clock is itself the bot signal and the
|
||||
# challenge then never clears — measurement in entrypoint.sh. Unset falls
|
||||
# back to the host's /etc/timezone below, which is a real zone whenever
|
||||
# the host clock is local; set BROWSER_TZ when the host runs UTC.
|
||||
TZ: ${BROWSER_TZ:-}
|
||||
volumes:
|
||||
# The zone *name*, which is what Chrome's ICU needs — see entrypoint.sh.
|
||||
# Absent on a non-Debian host, which the entrypoint handles by falling back to UTC.
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
# Cloudflare clearance must survive Chrome reaping and image recreation.
|
||||
- chrome-profile:/home/chrome/profile
|
||||
# Bound to the tailnet address only, never 0.0.0.0. CDP authenticates
|
||||
# nothing: whatever reaches this port drives the browser and, through it,
|
||||
# this host. On the VPS the safety was Docker network membership; here the
|
||||
# machine has a real LAN, so the bind address *is* the access control,
|
||||
# backed by Tailscale device identity. No default — an unset variable must
|
||||
# fail the deploy rather than silently publish CDP to the LAN.
|
||||
ports:
|
||||
- "${BROWSER_BIND_ADDR:?set BROWSER_BIND_ADDR to this machine's tailnet IP}:9222:9222"
|
||||
# Reaps zombie renderer processes, which otherwise accumulate for the
|
||||
# container's lifetime.
|
||||
init: true
|
||||
# Chrome allocates shared memory per tab and dies on Docker's 64MB default.
|
||||
# 128MB against a measured 19MB peak: the old 1GB reservation was sized by
|
||||
# superstition, and this box has 1.8GB total.
|
||||
shm_size: '128mb'
|
||||
# The browser is the newcomer on a machine where a Gitea runner already
|
||||
# holds ~1.2GiB of 1.8GiB. Load-bearing, not decorative: untuned Chrome
|
||||
# peaked at 645MiB cgroup, which is more than is free here.
|
||||
#
|
||||
# memswap_limit is memory+swap combined, so this allows 512MiB of swap —
|
||||
# Chrome reclaims its own cold pages onto this box's 5.9GiB of SATA swap
|
||||
# instead of taking resident memory from the runner.
|
||||
mem_limit: 512m
|
||||
memswap_limit: 1g
|
||||
# If the box does run out, the kernel takes the browser and never CI.
|
||||
oom_score_adj: 800
|
||||
# A challenge solve yields to a running build. Cold start degrades to ~3s
|
||||
# at half a CPU, immaterial against a 45-second challenge budget.
|
||||
cpu_shares: 512
|
||||
|
||||
volumes:
|
||||
chrome-profile:
|
||||
Reference in New Issue
Block a user