Move the browser off the VPS to its own unit (#46) #52
@@ -273,6 +273,17 @@ above.
|
|||||||
Do this after §2, on the second machine. Both machines must already be on the
|
Do this after §2, on the second machine. Both machines must already be on the
|
||||||
same tailnet.
|
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:**
|
**On the home machine:**
|
||||||
|
|
||||||
```bash
|
```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
|
tailscale ip -4 # -> 100.x.y.z, this machine's tailnet IP
|
||||||
cp .env.example .env
|
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
|
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
|
real LAN, so `0.0.0.0` would be a hole punched into your home network. Compose
|
||||||
refuses to start rather than guess.
|
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:
|
Prove the bind is tight, from the home machine itself:
|
||||||
|
|
||||||
```bash
|
```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
|
asleep, unreachable, or mid-power-outage costs chapter freshness and nothing
|
||||||
visible.
|
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
|
**Updating the browser** is independent of the API stack and has its own
|
||||||
cd ~/mangaBookmark/chrome && git pull && docker compose up -d --build
|
runbook — `REDEPLOY.md` §8.
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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 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. |
|
| 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. |
|
| 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:
|
Full first-time setup: `DEPLOY.md`. The one-off SQLite→Postgres move:
|
||||||
`CUTOVER.md`. Config reference and endpoints: `README.md`.
|
`CUTOVER.md`. Config reference and endpoints: `README.md`.
|
||||||
|
|||||||
+7
-7
@@ -13,14 +13,14 @@
|
|||||||
#
|
#
|
||||||
# For a throwaway local test, 127.0.0.1 is fine — but then only this machine
|
# 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.
|
# 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 —
|
# Clock zone the browser reports. Any real zone works and it need not match
|
||||||
# Cloudflare treats it as the datacenter default — and kagane's challenge then
|
# the egress IP's country — but it must not be UTC, which is itself the bot
|
||||||
# never clears. Measured 2026-08-08, identical container, one Indonesian egress
|
# signal that stops the challenge clearing. The measurement is in entrypoint.sh.
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# Unset falls back to the host's /etc/timezone, which is a real zone whenever
|
# 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
|
# the host clock is set to local time. Set this when the host runs UTC — a UTC
|
||||||
|
|||||||
@@ -17,14 +17,10 @@ services:
|
|||||||
container_name: bookmark-browser
|
container_name: bookmark-browser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# A UTC clock is itself the bot signal: Cloudflare treats it as the
|
# Any real zone works, but a UTC clock is itself the bot signal and the
|
||||||
# datacenter default, and kagane's challenge then never clears. Measured
|
# challenge then never clears — measurement in entrypoint.sh. Unset falls
|
||||||
# 2026-08-08, identical container, one Indonesian egress IP: UTC never
|
# back to the host's /etc/timezone below, which is a real zone whenever
|
||||||
# cleared in 60s (twice); Asia/Jakarta and America/New_York both cleared
|
# the host clock is local; set BROWSER_TZ when the host runs UTC.
|
||||||
# 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.
|
|
||||||
TZ: ${BROWSER_TZ:-}
|
TZ: ${BROWSER_TZ:-}
|
||||||
volumes:
|
volumes:
|
||||||
# The zone *name*, which is what Chrome's ICU needs — see entrypoint.sh.
|
# The zone *name*, which is what Chrome's ICU needs — see entrypoint.sh.
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ services:
|
|||||||
bookmark-api:
|
bookmark-api:
|
||||||
# Traffic arrives over the Traefik network, not a published port.
|
# Traffic arrives over the Traefik network, not a published port.
|
||||||
ports: !reset []
|
ports: !reset []
|
||||||
# `networks:` here replaces the base file's list entirely, so both must be
|
# Compose *merges* this list with the base file's, so the service ends up on
|
||||||
# named: `proxy` for Traefik routing, and `db` (defined in the base file)
|
# `default`, `db` and `proxy` — only the addition is named here. Do not
|
||||||
# to keep reaching Postgres without putting it on `proxy`. `proxy` also
|
# "tidy" the base file down to `db` on the strength of `proxy` being present:
|
||||||
# carries the poller's outbound traffic — `db` is `internal: true`, so a
|
# `db` is `internal: true`, and egress comes from `default`.
|
||||||
# container on it alone has no egress at all.
|
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
- db
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=${PROXY_NETWORK:-proxy}"
|
- "traefik.docker.network=${PROXY_NETWORK:-proxy}"
|
||||||
|
|||||||
+1
-2
@@ -86,8 +86,7 @@ services:
|
|||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
# `default` is not decoration: `db` is `internal: true`, and a container on
|
# `default` is not decoration: `db` is `internal: true`, and a container on
|
||||||
# nothing but an internal network gets neither a published port nor egress
|
# nothing but an internal network gets neither a published port nor egress
|
||||||
# — which would silently kill every poller fetch. The removed `browser`
|
# — which would silently kill every poller fetch.
|
||||||
# network used to be what supplied both.
|
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- db
|
- db
|
||||||
|
|||||||
Reference in New Issue
Block a user