From 15382eb603ef6b3a7c526b30ca5072462bf50752 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Sun, 9 Aug 2026 15:24:22 +0700 Subject: [PATCH] Fix review findings from the browser relocation (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- DEPLOY.md | 41 +++++++++++++++++++++++++++++---------- REDEPLOY.md | 2 +- chrome/.env.example | 14 ++++++------- chrome/docker-compose.yml | 12 ++++-------- docker-compose.prod.yml | 10 ++++------ docker-compose.yml | 3 +-- 6 files changed, 48 insertions(+), 34 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 664d219..892f5b5 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -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 ~/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. --- diff --git a/REDEPLOY.md b/REDEPLOY.md index b2d4d8b..102fade 100644 --- a/REDEPLOY.md +++ b/REDEPLOY.md @@ -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`. diff --git a/chrome/.env.example b/chrome/.env.example index af561f6..f77bf50 100644 --- a/chrome/.env.example +++ b/chrome/.env.example @@ -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 diff --git a/chrome/docker-compose.yml b/chrome/docker-compose.yml index 7398c86..59181a6 100644 --- a/chrome/docker-compose.yml +++ b/chrome/docker-compose.yml @@ -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. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 318bc35..7c8e628 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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}" diff --git a/docker-compose.yml b/docker-compose.yml index cf86a1d..7265963 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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