08749df050
Swap modernc.org/sqlite for jackc/pgx/v5 with no observable change: same endpoints, same wire format, same updated_at ordering rule. The schema now comes from numbered SQL embedded in the binary and applied on startup, one transaction each, recorded in schema_migrations. That replaces two pieces of SQLite-era machinery, both deleted rather than ported: the column probing (Postgres has ADD COLUMN IF NOT EXISTS, and there is no legacy database left to probe) and the Asura key rewrite, which has run clean on every start for months now that the userscripts strip build hashes before writing. Its regexp survives as latest.asuraBuildHash, where the poller still needs it to scope chapter links to a series whose slug carries a rotating hash. Types get real: favorite is a boolean, chapter numbers double precision, timestamps stay unix-ms bigint. SQLite's null-safe IS NOT becomes IS DISTINCT FROM, which is what implements the rule that only reading progress reorders a list. Inside COALESCE/NULLIF the status and kind parameters need an explicit ::text -- there is no target column to infer from and Postgres refuses to guess. Tests lose their free t.TempDir() database, so Docker is now a hard prerequisite for `go test ./...`: internal/pgtest starts one postgres:17-alpine per test binary and hands each test a database of its own. Also lands CONTEXT.md and the four ADRs written while scoping #18. BREAKING CHANGE: DB_PATH is retired for DATABASE_URL, which is required and has no default. Compose gains a postgres service on an internal network with its own volume; POSTGRES_PASSWORD joins .env. The old bookmarks-data volume is deliberately left undeclared so `docker compose down -v` cannot take the pre-migration database with it. main is not deployable until #25 and #26 land. Closes #20 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
298 lines
11 KiB
Markdown
298 lines
11 KiB
Markdown
# Deployment
|
|
|
|
Step-by-step for the backend (Docker + Traefik) and the Bromite userscript.
|
|
Assumes you already run Traefik in Docker with a working HTTPS entrypoint and an
|
|
ACME/cert resolver, and control a domain.
|
|
|
|
---
|
|
|
|
## 0. Prerequisites
|
|
|
|
- Docker + Docker Compose on the server.
|
|
- A Traefik instance watching a Docker network (default name assumed: `proxy`).
|
|
- DNS: an `A`/`AAAA` record for `bookmark-api.<yourdomain>` pointing at the server.
|
|
- The repo copied to the server, e.g. `/opt/bookmarkmanager/` (needs `backend/`,
|
|
`docker-compose.yml`, `docker-compose.prod.yml`, `.env.example`).
|
|
|
|
Confirm the Traefik network exists (create if not):
|
|
|
|
```bash
|
|
docker network ls | grep proxy || docker network create proxy
|
|
```
|
|
|
|
---
|
|
|
|
## 1. Configure `.env`
|
|
|
|
```bash
|
|
cd /opt/bookmarkmanager
|
|
cp .env.example .env
|
|
```
|
|
|
|
Edit `.env`:
|
|
|
|
```ini
|
|
# Required — long random secret, also goes in the userscript.
|
|
API_TOKEN=<paste output of: openssl rand -hex 32>
|
|
|
|
# CORS allowlist — leave as-is unless a site changes hostname.
|
|
ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to
|
|
|
|
# Required — password for the bundled Postgres container. Compose builds the
|
|
# backend's DATABASE_URL out of it and has no fallback for either.
|
|
POSTGRES_PASSWORD=<paste output of: openssl rand -hex 24>
|
|
|
|
# Leave unset. Only set this to point the backend at a Postgres compose does
|
|
# not run; it then replaces the URL built from POSTGRES_PASSWORD above.
|
|
# DATABASE_URL=postgres://user:pass@host:5432/bookmarks?sslmode=require
|
|
|
|
# Required for the Traefik override. Both have no fallback — compose refuses
|
|
# to start without them. BOOKMARK_WEB_HOST is required even if you never set
|
|
# WEB_PASSWORD; see 1b.
|
|
BOOKMARK_API_HOST=bookmark-api.violetcrown.my.id
|
|
BOOKMARK_WEB_HOST=bookmark.violetcrown.my.id
|
|
|
|
# Only if your Traefik setup differs from these defaults:
|
|
# PROXY_NETWORK=proxy
|
|
# TRAEFIK_ENTRYPOINT=websecure
|
|
# TRAEFIK_CERTRESOLVER=le
|
|
```
|
|
|
|
Generate + insert the two secrets in three lines:
|
|
|
|
```bash
|
|
sed -i "s|^API_TOKEN=.*|API_TOKEN=$(openssl rand -hex 32)|" .env
|
|
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" .env
|
|
grep -E '^API_TOKEN=' .env # copy this — the userscript needs the same value
|
|
```
|
|
|
|
`POSTGRES_PASSWORD` is read **only while the `postgres-data` volume is empty**,
|
|
which in practice means at first boot. Changing it afterwards changes the URL
|
|
the backend dials but not the password the database expects, and `bookmark-api`
|
|
crash-loops on `password authentication failed`. Set it before §2 and leave it
|
|
alone.
|
|
|
|
> Match `TRAEFIK_ENTRYPOINT` / `TRAEFIK_CERTRESOLVER` to your Traefik's actual
|
|
> names (check your Traefik static config — common alternatives: `https`,
|
|
> `myresolver`, `cloudflare`). Wrong names = no certificate issued.
|
|
|
|
---
|
|
|
|
## 1b. Web UI
|
|
|
|
The browser UI is served by the same container on a second hostname.
|
|
|
|
1. Add a DNS `A`/`AAAA` record for `bookmark.<yourdomain>` pointing at the server —
|
|
the same address as `bookmark-api.<yourdomain>`.
|
|
|
|
2. Set both variables in `.env`:
|
|
|
|
```ini
|
|
BOOKMARK_WEB_HOST=bookmark.violetcrown.my.id
|
|
WEB_PASSWORD=<paste output of: openssl rand -base64 18>
|
|
```
|
|
|
|
Generate and insert in one line:
|
|
|
|
```bash
|
|
sed -i "s|^WEB_PASSWORD=.*|WEB_PASSWORD=$(openssl rand -base64 18)|" .env
|
|
grep -E '^WEB_PASSWORD=' .env # this is what you type into the site
|
|
```
|
|
|
|
3. Redeploy and check:
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
|
curl -s -o /dev/null -w '%{http_code}\n' https://bookmark.violetcrown.my.id/
|
|
```
|
|
|
|
Expected `200`, serving the login page.
|
|
|
|
Leaving `WEB_PASSWORD` unset is safe: the web routes are not registered and `/`
|
|
returns 404. The userscript's API on `BOOKMARK_API_HOST` is unaffected either way.
|
|
|
|
`BOOKMARK_WEB_HOST` itself is required by the prod override regardless — like
|
|
`BOOKMARK_API_HOST`, its Traefik label has no fallback, so `docker compose up`
|
|
refuses to start without it even if `WEB_PASSWORD` is unset and the web UI is
|
|
otherwise dormant.
|
|
|
|
Sessions are signed with a key derived from `API_TOKEN` and `WEB_PASSWORD`, so
|
|
rotating either one logs every browser out. The session cookie lasts 60 days.
|
|
|
|
---
|
|
|
|
## 2. Build + start
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
|
```
|
|
|
|
This merges the base file (build/image/env/volume) with the prod override
|
|
(no host port, Traefik network + router labels). Always pass **both** `-f`
|
|
flags — the prod file is not standalone.
|
|
|
|
Three services come up: `bookmark-api` (the backend), `postgres` (its database,
|
|
`postgres:17-alpine`), and `headless-shell`, a CDP sidecar the poller uses to
|
|
fetch kagane (behind a Cloudflare JS challenge). Neither of the latter two
|
|
publishes a port: `postgres` sits alone with `bookmark-api` on an
|
|
`internal: true` network, and `headless-shell` is reachable only over
|
|
`BROWSER_WS_URL`. A missing headless-shell just makes the poller skip kagane and
|
|
log it. A missing Postgres stops everything — `bookmark-api` waits for
|
|
`pg_isready` to pass, then applies its embedded migrations, and only then
|
|
listens. The schema is created that way; there is nothing to import by hand.
|
|
|
|
Check it's up and healthy:
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
|
|
# bookmark-api Up; postgres Up (healthy)
|
|
docker logs bookmark-api --tail 20 # expect: "listening on :8080 ..."
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Verify over HTTPS
|
|
|
|
Give Traefik a few seconds to issue the cert, then:
|
|
|
|
```bash
|
|
# Health (no auth) — must be valid TLS, no cert warning.
|
|
curl -s https://bookmark-api.violetcrown.my.id/healthz # -> ok
|
|
|
|
# Auth enforced.
|
|
curl -s -o /dev/null -w '%{http_code}\n' \
|
|
https://bookmark-api.violetcrown.my.id/bookmarks # -> 401
|
|
|
|
TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2)
|
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
https://bookmark-api.violetcrown.my.id/bookmarks # -> []
|
|
|
|
# CORS preflight from a real site origin.
|
|
curl -s -i -X OPTIONS \
|
|
-H 'Origin: https://asurascans.com' \
|
|
-H 'Access-Control-Request-Method: PUT' \
|
|
https://bookmark-api.violetcrown.my.id/bookmarks/x | grep -i access-control
|
|
# -> Access-Control-Allow-Origin: https://asurascans.com (+ Methods/Headers)
|
|
```
|
|
|
|
All four must pass. Valid TLS is non-negotiable — the manga sites are HTTPS, so
|
|
a bad cert makes the browser block the userscript's `fetch()` (mixed content).
|
|
|
|
---
|
|
|
|
## 4. Configure the userscript
|
|
|
|
Edit the config block at the top of `userscript/manga-bookmark.user.js`:
|
|
|
|
```js
|
|
const API_BASE = "https://bookmark-api.yourdomain.com"; // no trailing slash
|
|
const API_TOKEN = "<same token as .env>";
|
|
```
|
|
|
|
The token sits in the userscript's isolated world — the manga sites' JS can't
|
|
read it.
|
|
|
|
Also edit the `@downloadURL`/`@updateURL` metadata lines near the top of the
|
|
file — they ship hardcoded to this deployment's domain and token, so a
|
|
deployer who skips them ends up auto-updating from someone else's backend.
|
|
See "Installing / updating the userscript" below for how those two lines are
|
|
used.
|
|
|
|
---
|
|
|
|
## 5. Install on Bromite
|
|
|
|
1. Bromite → **Settings → User scripts** → enable (accept the permission prompt).
|
|
2. Put the edited `manga-bookmark.user.js` on the device (save the file, or open
|
|
its raw URL). Bromite detects `.user.js` and offers to install.
|
|
3. Confirm install — the `@match` list covers both sites.
|
|
4. Open a series on asurascans.com or demonicscans.org → a 📑 button appears
|
|
bottom-right → tap → **+ Bookmark this**.
|
|
|
|
Optional desktop test: the script is `GM_*`-free, so the same file installs in
|
|
Tampermonkey/Violentmonkey for quick checks before going mobile.
|
|
|
|
---
|
|
|
|
## 6. Smoke-test the full loop
|
|
|
|
1. Bookmark a series on Asura.
|
|
2. `curl -s -H "Authorization: Bearer $TOKEN" https://bookmark-api.yourdomain.com/bookmarks`
|
|
on the server — the series should appear.
|
|
3. Open a chapter of that series — reopen the panel; last-read updates to that
|
|
chapter (auto, never regresses on older chapters).
|
|
4. Open Demonic, open the panel — the Asura bookmark shows there too (shared
|
|
store, cross-site unified list).
|
|
|
|
---
|
|
|
|
## Updating
|
|
|
|
Pull new code, then rebuild:
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
|
```
|
|
|
|
Data persists in the named volume `postgres-data` across rebuilds. (If this
|
|
server predates the Postgres migration, the old SQLite volume `bookmarks-data`
|
|
is still on disk and deliberately undeclared in compose so `down -v` cannot take
|
|
it; see `REDEPLOY.md` §1 for when to remove it.)
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Likely cause / fix |
|
|
|---------|--------------------|
|
|
| No cert / TLS error at the domain | `TRAEFIK_ENTRYPOINT` or `TRAEFIK_CERTRESOLVER` name wrong; or DNS not resolving yet. Check `docker logs <traefik>`. |
|
|
| 404 from Traefik | Service not on the `proxy` network, or `BOOKMARK_API_HOST` mismatch. Confirm `docker network inspect proxy` lists `bookmark-api`. |
|
|
| `fetch` fails in the userscript, `curl` works | Origin missing from `ALLOWED_ORIGINS`, or mixed content (backend not HTTPS). |
|
|
| 401 with the right token | Trailing space/newline in `API_TOKEN`; regenerate and restart. |
|
|
| Panel button absent | URL didn't match an adapter, or user scripts disabled in Bromite. |
|
|
| `compose ... config` errors about `API_TOKEN` or `POSTGRES_PASSWORD` | Run compose from the dir with `.env`, or export the vars. Both are required and neither has a fallback. |
|
|
| `bookmark-api` restarts in a loop, `password authentication failed for user "bookmarks"` | `POSTGRES_PASSWORD` was changed after first boot; Postgres only applies it to an empty `postgres-data`. Restore the old value, or reset the role (`REDEPLOY.md` troubleshooting). |
|
|
| `bookmark-api` never logs `listening on :8080` | It is blocked on `postgres` passing `pg_isready`, or a migration failed. `docker compose -f docker-compose.yml -f docker-compose.prod.yml logs postgres`. |
|
|
|
|
Backend config reference and endpoint list: see `README.md`.
|
|
|
|
---
|
|
|
|
## Installing / updating the userscript
|
|
|
|
The backend serves the script itself, so Violentmonkey can auto-update it.
|
|
Complements §4 above — that step points `API_BASE`/`API_TOKEN` at your
|
|
backend; this one points `@downloadURL`/`@updateURL` at the same place so
|
|
auto-updates come from it too.
|
|
|
|
Install once, on the phone (Cromite + Violentmonkey):
|
|
|
|
```
|
|
https://bookmark-api.<your-domain>/u/<API_TOKEN>/manga-bookmark.user.js
|
|
```
|
|
|
|
Open that URL in Cromite; Violentmonkey offers to install it. The token is in
|
|
the path because Violentmonkey's update poll sends no `Authorization` header,
|
|
and the script embeds `API_TOKEN` in plain text — an open URL would leak it. A
|
|
wrong token answers 404.
|
|
|
|
Updating, without a redeploy:
|
|
|
|
```bash
|
|
vi userscript/manga-bookmark.user.js # on the VPS, in this checkout
|
|
```
|
|
|
|
`./userscript` is bindmounted read-only into the container and read fresh on
|
|
every request, so the edit is live immediately. The served `@version` is derived
|
|
from the file's mtime (`YYYY.MM.DD.HHMM`, UTC), not from the `@version` in the
|
|
file, so any edit outranks the installed copy and Violentmonkey pulls it on its
|
|
next check. The `@version` in the repo is a human marker only.
|
|
|
|
Updating via redeploy: `git pull` overwrites the file with the committed
|
|
version, which is the intended behaviour — a deploy always ships the repo's
|
|
script. Note that `git pull` sets mtime to checkout time, so even a rollback
|
|
serves a *higher* version and is adopted.
|
|
|
|
If the mount is missing, the endpoint answers 404 and logs it; bookmark sync is
|
|
unaffected.
|