# Book & Comic Library Personal book/comic library: Go+Gin backend (scan/upload ingestion, multi-user JWT, reading progress, disk+Redis cache) + Docker Compose deployment. Design: `superpowers/specs/2026-09-04-book-comic-library-design.md`; deployment spec: `superpowers/specs/2026-09-07-docker-deploy-spec-design.md`. Chinese mirror: `README_zh.md`. ## Deploy mode - release: `deploy/docker-compose.yml` — multi-stage `backend|frontend/Dockerfile.prod` (target runner) compiled artifacts, no source mounts; infra ports stay on the internal network. - dev: `deploy/docker-compose.dev.yml` — all four services containerized, source mounted as `../backend:/app` and `../frontend:/app` (frontend with an anonymous `node_modules` volume), `target: dev` images; api runs `go mod download && dlv debug ./cmd/webui` (hot restart without rebuild, delve :2345); infra exposed to host PG 5432 / Redis 6379; healthcheck-gated startup. ## Volume Mount - Shared named volumes: `booklib_postgres_data`, `booklib_redis_data` (identical names in both composes, so dev/release see the same data; only `down -v` clears them). - Config/log bind mounts: `deploy/nginx/{nginx.conf,conf.d/default.conf}`, `deploy/redis/redis.conf` (`:ro`) and `deploy/logs/nginx` — all rendered by `deploy/prepare.sh` (templates are the `*.tpl` files beside each output); **wrong/missing config in the container = you forgot to rerun it**. - File storage: `deploy/api/storage` → container `/data/books` (cache writes to `/data/books/cache`). ## Run it (production shape) ```bash cp deploy/.env.example deploy/.env # set JWT_SECRET, ADMIN_USER, ADMIN_PASSWORD (>= 8 chars; seed skips and logs below 8) deploy/prepare.sh docker compose -f deploy/docker-compose.yml up -d --build bash scripts/smoke.sh && bash scripts/smoke-web.sh ``` - web: `http://localhost:8080` (change via `WEB_PORT`); the API goes through the nginx `/api/` prefix reverse proxy to stateless api replicas (`--scale api=N`). - Drop raw books into `deploy/api/storage/` (mounted at `/data/books`); the scanner ingests periodically (default 60s). - nginx access/error logs: `deploy/logs/nginx/`. ## Development ```bash deploy/prepare.sh docker compose -f deploy/docker-compose.dev.yml up -d --build ``` - Frontend: http://localhost:5173 (vite, HMR works directly; `/api` proxied to the in-container api). - After editing Go code: `docker compose -f deploy/docker-compose.dev.yml restart api` (recompiles the mounted source, no rebuild). - Breakpoint debugging: delve headless at `localhost:2345` (VSCode launch: `{"type":"go","request":"attach","mode":"remote","host":"localhost","port":2345,"substitutePath":[{"from":"${workspaceFolder}/backend","to":"/app"}]}`; continue via dlv commands after hitting a breakpoint). - Run tests against directly reachable infra: PG `localhost:5432` (lib/lib/lib), Redis `localhost:6379`: ```bash cd backend export DATABASE_URL='postgres://lib:lib@localhost:5432/lib?sslmode=disable' export REDIS_URL='redis://localhost:6379' go vet ./... && gofmt -l . go test -p 1 -count=1 ./... ``` `-p 1` is required: integration tests share one PG database and each clears tables with `DELETE FROM ...` — running in parallel deletes each other's data and fails randomly. Bring the dev stack down with `down` (not `rm`), or the anonymous node_modules volume becomes an orphan. Tests that need PG/Redis skip automatically when absent; Redis downtime doesn't break functionality (the whole chain degrades to miss/passthrough, see spec §9). Frontend gate: `cd frontend && npm run check` (tsc + vitest + vite build). ## Old-volume migration (one-off, upgrading from the previous deploy layout) ```bash # old PG data → new shared volume docker run --rm -v book-comic-library_pgdata:/from -v booklib_postgres_data:/to alpine cp -a /from/. /to/ # the old cache volume is derived data (covers/unzipped pages), just drop it (auto-rebuilt) docker volume rm book-comic-library_pgdata book-comic-library_cache ``` Book files: move the contents of the old host `./library/` into `deploy/api/storage/`. ## Trusted proxies & rate limiting - nginx lives inside the compose network, so api's `ClientIP` only trusts `TRUSTED_PROXY_CIDRS` (comma-separated CIDRs, default `172.16.0.0/12`, the compose subnet). Spoofed external `X-Forwarded-For` can't bypass rate-limit buckets; change this env when the deploy network changes. - Login is limited to 5 attempts/min/IP **counted per attempt, successful logins included** — brute force and normal high-frequency login share the budget. ## Read this before changing the schema `db.Migrate` only runs the `CREATE TABLE IF NOT EXISTS` statements of `schema.sql` — column adds/changes **do not take effect** on existing databases. Before any column change, introduce a `schema_migrations` version table + ordered migrations, otherwise old deployments silently run on the old shape. ## PWA Immutable assets (covers/CBZ pages/raw files) are SW cache-first; read content stays available offline; logging out clears the SW cache.