Move the browser off the VPS to its own unit (#46) #52

Merged
sulthan merged 2 commits from feat/46-remote-browser into main 2026-08-09 15:28:22 +07:00
6 changed files with 48 additions and 34 deletions
Showing only changes of commit 15382eb603 - Show all commits
+31 -10
View File
@@ -273,6 +273,17 @@ above.
Do this after §2, on the second machine. Both machines must already be on the
same tailnet.
First, on the VPS, record what you are reclaiming — this is the whole point of
the move and there is no way to measure it afterwards:
```bash
free -m | awk '/^Mem:/ {print "available before:", $NF, "MiB"}'
```
Take it again after §7 is finished and the old sidecar is gone. Expect roughly
the sidecar's former footprint back (measured at 471 MiB working set, 595 MiB
cgroup).
**On the home machine:**
```bash
@@ -280,7 +291,7 @@ git clone <this repo> ~/mangaBookmark && cd ~/mangaBookmark/chrome
tailscale ip -4 # -> 100.x.y.z, this machine's tailnet IP
cp .env.example .env
sed -i "s|^BROWSER_BIND_ADDR=.*|BROWSER_BIND_ADDR=$(tailscale ip -4)|" .env
echo "BROWSER_BIND_ADDR=$(tailscale ip -4)" >> .env
docker compose up -d --build
```
@@ -296,6 +307,21 @@ On the VPS that job was done by Docker network membership; this machine has a
real LAN, so `0.0.0.0` would be a hole punched into your home network. Compose
refuses to start rather than guess.
**Narrow it to the one device that needs it.** The bind address keeps CDP off
your LAN; it still leaves port 9222 open to every device on the tailnet, and
CDP has no login. Add a rule in the Tailscale admin console's access controls
so only the VPS can reach it — tag the two machines, then:
```jsonc
// tailnet policy file
"acls": [
{ "action": "accept", "src": ["tag:bookmark-api"], "dst": ["tag:bookmark-browser:9222"] },
]
```
Without a rule the tailnet default is allow-all, so this step is what makes
"Tailscale identity is the access control" true rather than aspirational.
Prove the bind is tight, from the home machine itself:
```bash
@@ -337,16 +363,11 @@ a cover is stored it is served from Postgres forever after, so the browser being
asleep, unreachable, or mid-power-outage costs chapter freshness and nothing
visible.
**Updating the browser** is independent of the API stack:
Finally, take the VPS `free -m` reading again and compare it against the one
from the top of this section.
```bash
cd ~/mangaBookmark/chrome && git pull && docker compose up -d --build
```
Rebuild is the Chrome upgrade path — the image installs `google-chrome-stable`
unpinned on purpose, because a stale browser is exactly what Cloudflare turns
away. The `chrome-profile` volume survives the rebuild, so the clearance cookies
are reused instead of re-solved.
**Updating the browser** is independent of the API stack and has its own
runbook — `REDEPLOY.md` §8.
---
+1 -1
View File
@@ -457,7 +457,7 @@ and skipped, stored covers still served.
| kagane rows stopped updating after a redeploy | Check `BROWSER_WS_URL` survived the `.env` edit and still names the home machine's tailnet **IP**. A hostname 500s at `/json/version`; an empty value disables the browser silently. Plain-TLS sites keep working either way, which is why this is easy to miss. |
| kagane covers went blank in the web UI | They should not — covers are rows in `covers`, not an in-process cache. `$COMPOSE exec -T postgres psql -U bookmarks -d bookmarks -c 'select count(*) from covers'`. Zero after a restore means the dump predates the covers table; they refill on the next poll of each series. |
| Browser unit will not start: `set BROWSER_BIND_ADDR to this machine's tailnet IP` | `chrome/.env` is missing or the variable is empty. It has no default on purpose — an unset value must fail the deploy rather than publish an unauthenticated CDP port to the LAN. |
| `bookmark-browser` shows `OOMKilled true` | The 512 MiB cap did its job. Read `docker logs bookmark-browser` before raising it: the cap is sized against a measured 645 MiB untuned peak and exists so the kernel never takes the Gitea runner instead. |
| `bookmark-browser` shows `OOMKilled true` | The cap did its job. Read `docker logs bookmark-browser` before raising it — the sizing and what the cap protects are in ADR-0006. |
Full first-time setup: `DEPLOY.md`. The one-off SQLite→Postgres move:
`CUTOVER.md`. Config reference and endpoints: `README.md`.
+7 -7
View File
@@ -13,14 +13,14 @@
#
# For a throwaway local test, 127.0.0.1 is fine — but then only this machine
# can reach it, so the API must run here too.
BROWSER_BIND_ADDR=100.x.y.z
# Left commented so `cp .env.example .env && docker compose up` fails with the
# variable's own message telling you what to set, rather than Docker rejecting
# "100.x.y.z" as an invalid IP.
# BROWSER_BIND_ADDR=100.x.y.z
# Clock zone the browser reports. A UTC clock is itself the bot signal —
# Cloudflare treats it as the datacenter default — and kagane's challenge then
# never clears. Measured 2026-08-08, identical container, one Indonesian egress
# IP: UTC never cleared in 60s (twice); Asia/Jakarta and America/New_York both
# cleared in 4s. So any real zone works; it does not have to match the IP's
# country, it just must not be UTC.
# Clock zone the browser reports. Any real zone works and it need not match
# the egress IP's country — but it must not be UTC, which is itself the bot
# signal that stops the challenge clearing. The measurement is in entrypoint.sh.
#
# Unset falls back to the host's /etc/timezone, which is a real zone whenever
# the host clock is set to local time. Set this when the host runs UTC — a UTC
+4 -8
View File
@@ -17,14 +17,10 @@ services:
container_name: bookmark-browser
restart: unless-stopped
environment:
# A UTC clock is itself the bot signal: Cloudflare treats it as the
# datacenter default, and kagane's challenge then never clears. Measured
# 2026-08-08, identical container, one Indonesian egress IP: UTC never
# cleared in 60s (twice); Asia/Jakarta and America/New_York both cleared
# in 4s. So any real zone works and it need not match the IP's country —
# only UTC fails. Unset falls back to the host's /etc/timezone below,
# which is a real zone whenever the host clock is set to local time; set
# BROWSER_TZ when the host runs UTC.
# 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.
+4 -6
View File
@@ -17,14 +17,12 @@ services:
bookmark-api:
# Traffic arrives over the Traefik network, not a published port.
ports: !reset []
# `networks:` here replaces the base file's list entirely, so both must be
# named: `proxy` for Traefik routing, and `db` (defined in the base file)
# to keep reaching Postgres without putting it on `proxy`. `proxy` also
# carries the poller's outbound traffic — `db` is `internal: true`, so a
# container on it alone has no egress at all.
# Compose *merges* this list with the base file's, so the service ends up on
# `default`, `db` and `proxy` — only the addition is named here. Do not
# "tidy" the base file down to `db` on the strength of `proxy` being present:
# `db` is `internal: true`, and egress comes from `default`.
networks:
- proxy
- db
labels:
- "traefik.enable=true"
- "traefik.docker.network=${PROXY_NETWORK:-proxy}"
+1 -2
View File
@@ -86,8 +86,7 @@ services:
- "127.0.0.1:8080:8080"
# `default` is not decoration: `db` is `internal: true`, and a container on
# nothing but an internal network gets neither a published port nor egress
# — which would silently kill every poller fetch. The removed `browser`
# network used to be what supplied both.
# — which would silently kill every poller fetch.
networks:
- default
- db