2a3bb6922d
Closes #46 once deployed. The headless browser leaves the API stack and becomes its own compose unit (`chrome/docker-compose.yml`) intended for the home machine, reached over the tailnet. No fallback sidecar is left on the VPS. The backend needs no code change — `BROWSER_WS_URL` was already the only coupling. Its default is now empty rather than a pinned Docker IP, so an unconfigured or unreachable browser degrades exactly as it always has: plain-TLS libraries unaffected, kagane/novelfull logged and skipped, stored covers still served. ### What shipped - `chrome/docker-compose.yml` + `chrome/.env.example` — the browser unit, with the CDP port bound to `${BROWSER_BIND_ADDR}` (no default) and the resource limits from the epic: 512 MiB / 1 GiB memory+swap, `oom_score_adj 800`, halved CPU weight, shm 1 GiB -> 128 MiB. - API stack drops the service, its `depends_on` and the `browser` network. - `bookmark-api` gains the `default` network. Dropping `browser` had left it on `db` alone, which is `internal: true` — no published port and, worse, no egress for the poller at all. Caught by actually bringing the stack up. - ADR-0006 for the topology; `DEPLOY.md` §7 for first-time setup of the browser machine; `REDEPLOY.md` §8 for its independent update cadence; architecture diagrams, config tables and troubleshooting rows across README/AGENTS/env. ### Verified locally - Browser unit builds and runs: Chrome 151, UA carries no `HeadlessChrome`, all limits applied as declared. - **Live smoke passes through the new unit**: `TestSmokeKaganeImage` fetched 56710 bytes of `image/webp`, `TestSmokeKaganeGet` got a 200 with a real chapter list. The challenge cleared under the reduced 128 MiB shm. - Bind isolation proven: refused on the host's non-loopback address, accepted on the configured one. - 321 MiB peak of the 512 MiB cap after a full solve; 0 restarts, no OOM kill. - API stack comes up clean, `/healthz` 200; egress confirmed present on `default` and absent on `db`. - `go test ./...`, `go vet`, `gofmt` clean. ### Left to the operator Provisioning the home machine, the Tailscale ACL, setting `BROWSER_WS_URL` in production, and observing acceptance criteria 5-7 (covers with the machine off, several days of zero OOM/restarts, VPS memory improvement). `DEPLOY.md` §7 now carries the before/after `free -m` reading those need. Reviewed-on: #52 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
54 lines
2.7 KiB
YAML
54 lines
2.7 KiB
YAML
# Production override: join an existing Traefik network and let Traefik route
|
|
# bookmark-api.<domain> -> this service with TLS. No host port published.
|
|
#
|
|
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
|
#
|
|
# Set in .env:
|
|
# BOOKMARK_API_HOST=bookmark-api.example.com # your subdomain (required)
|
|
# BOOKMARK_WEB_HOST=bookmark.example.com # browser UI subdomain, same container (required)
|
|
# PROXY_NETWORK=proxy # Traefik's network name, if not "proxy"
|
|
# TRAEFIK_ENTRYPOINT=websecure # your HTTPS entrypoint name
|
|
# TRAEFIK_CERTRESOLVER=le # your ACME/cert resolver name
|
|
#
|
|
# The network must already exist and Traefik must watch it:
|
|
# docker network create proxy # if it doesn't yet
|
|
|
|
services:
|
|
bookmark-api:
|
|
# Traffic arrives over the Traefik network, not a published port.
|
|
ports: !reset []
|
|
# 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
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=${PROXY_NETWORK:-proxy}"
|
|
- "traefik.http.routers.bmapi.rule=Host(`${BOOKMARK_API_HOST:?set BOOKMARK_API_HOST in .env}`)"
|
|
- "traefik.http.routers.bmapi.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
|
|
- "traefik.http.routers.bmapi.tls=true"
|
|
- "traefik.http.routers.bmapi.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
|
|
- "traefik.http.services.bmapi.loadbalancer.server.port=8080"
|
|
# Second hostname for the browser UI, same container. Traefik needs the
|
|
# service named explicitly once more than one router targets it.
|
|
- "traefik.http.routers.bmapi.service=bmapi"
|
|
- "traefik.http.routers.bmweb.rule=Host(`${BOOKMARK_WEB_HOST:?set BOOKMARK_WEB_HOST in .env}`)"
|
|
- "traefik.http.routers.bmweb.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
|
|
- "traefik.http.routers.bmweb.tls=true"
|
|
- "traefik.http.routers.bmweb.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
|
|
- "traefik.http.routers.bmweb.service=bmapi"
|
|
|
|
# No browser service here. It runs on the home machine as its own unit
|
|
# (chrome/docker-compose.yml) and is reached over the tailnet — see
|
|
# docs/adr/0006-browser-on-the-home-machine.md. It must never be given a
|
|
# service on this host: `proxy` is shared with whatever else sits behind
|
|
# Traefik, and an unauthenticated CDP endpoint on it is remote code
|
|
# execution for any of them.
|
|
|
|
networks:
|
|
proxy:
|
|
external: true
|
|
name: ${PROXY_NETWORK:-proxy}
|