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