4.9 KiB
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-stagebackend|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:/appand../frontend:/app(frontend with an anonymousnode_modulesvolume),target: devimages; api runsgo 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; onlydown -vclears them). - Config/log bind mounts:
deploy/nginx/{nginx.conf,conf.d/default.conf},deploy/redis/redis.conf(:ro) anddeploy/logs/nginx— all rendered bydeploy/prepare.sh(templates are the*.tplfiles 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)
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 viaWEB_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
deploy/prepare.sh
docker compose -f deploy/docker-compose.dev.yml up -d --build
- Frontend: http://localhost:5173 (vite, HMR works directly;
/apiproxied 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), Redislocalhost:6379:
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)
# 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
ClientIPonly trustsTRUSTED_PROXY_CIDRS(comma-separated CIDRs, default172.16.0.0/12, the compose subnet). Spoofed externalX-Forwarded-Forcan'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.