# 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: