Release / build-and-push (push) Successful in 3m6s
The 12:21 sync (cf73eec2) made the README block a byte-copy of
docker-compose.yml as it stood before the inline db-init landed;
reconcile to the current file: initdb.d mount dropped, inline
hBcescaped bootstrap, PGPORT in db-init env (DB_PORT is a variable in
every connection path). Block verified byte-identical to
docker-compose.yml; no secrets in either version (placeholders only).
208 lines
6.9 KiB
Markdown
208 lines
6.9 KiB
Markdown
# <img src="web/static/favicon.svg" width="32" alt="Bookhoard logo"> Bookhoard
|
|
|
|
A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and Tailwind CSS featuring **universal cross-device sync**, beautiful dark themes, and comprehensive media management.
|
|
|
|
## ✨ Why Bookhoard?
|
|
|
|
**🔄 Universal Sync**: Your reading position, bookmarks, highlights, and notes sync automatically between KOReader and the web - with native Kobo sync and mobile apps coming later.
|
|
|
|
**📱 Multi-Library**: Organize your ebooks, comics, and manga with per-library folders and smart collections.
|
|
|
|
**🎨 Beautiful UI**: 11 gorgeous dark themes with responsive design that works on any device.
|
|
|
|
**🔒 Secure**: JWT authentication, bcrypt password hashing, rate limiting, and no passwords on devices.
|
|
|
|
---
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### Prerequisites
|
|
|
|
- **Podman** (recommended) or Docker
|
|
- **5 minutes** of your time
|
|
|
|
### Installation (no source checkout needed)
|
|
|
|
Create a directory on the Docker host. You only need the compose file,
|
|
an `.env`, and an `uploads` folder — everything else rides the published
|
|
image.
|
|
|
|
```bash
|
|
mkdir bookhoard && cd bookhoard
|
|
mkdir uploads
|
|
```
|
|
|
|
**`docker-compose.yml`** — copy this wholesale:
|
|
|
|
```yaml
|
|
services:
|
|
# PostgreSQL Database
|
|
db:
|
|
image: postgres:15-alpine
|
|
container_name: bookhoard_db
|
|
environment:
|
|
POSTGRES_DB: bookhoard
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: ${DBPASS}
|
|
# PGPORT makes Postgres listen on DB_PORT (kept in sync with the host mapping + app's DATABASE_PORT)
|
|
PGPORT: ${DB_PORT:-5432}
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
ports:
|
|
- "127.0.0.1:${DB_PORT:-5432}:${DB_PORT:-5432}"
|
|
- "[::1]:${DB_PORT:-5432}:${DB_PORT:-5432}"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
env_file:
|
|
- .env
|
|
|
|
# One-shot bootstrap: creates the bookhoard_srv runtime role and
|
|
# transfers object ownership to it (RLS prerequisite). Idempotent —
|
|
# safe on every startup; the app waits for its success. Passwords
|
|
# accept inline values or .env, like the rest of this file.
|
|
# NOTE: plpgsql $$ tags are written $$$$ — compose eats one dollar
|
|
# per pair before the container sees the SQL.
|
|
db-init:
|
|
image: postgres:15-alpine
|
|
container_name: bookhoard_db_init
|
|
environment:
|
|
PGHOST: db
|
|
PGPORT: ${DB_PORT:-5432}
|
|
PGUSER: postgres
|
|
PGDATABASE: bookhoard
|
|
PGPASSWORD: ${DBPASS}
|
|
DBSRVPASS: ${DBSRVPASS}
|
|
entrypoint: ["bash", "-c"]
|
|
command:
|
|
- |
|
|
set -e
|
|
: "${DBSRVPASS:?DBSRVPASS required in .env}"
|
|
psql -v ON_ERROR_STOP=1 -v role_pass="$DBSRVPASS" <<'EOSQL'
|
|
DO $$$$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bookhoard_srv') THEN
|
|
CREATE ROLE bookhoard_srv LOGIN;
|
|
END IF;
|
|
END $$$$;
|
|
ALTER ROLE bookhoard_srv LOGIN PASSWORD :'role_pass';
|
|
DO $$$$
|
|
DECLARE r RECORD;
|
|
BEGIN
|
|
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP
|
|
EXECUTE format('ALTER TABLE public.%I OWNER TO bookhoard_srv', r.tablename);
|
|
END LOOP;
|
|
FOR r IN SELECT sequencename FROM pg_sequences WHERE schemaname = 'public' LOOP
|
|
EXECUTE format('ALTER SEQUENCE public.%I OWNER TO bookhoard_srv', r.sequencename);
|
|
END LOOP;
|
|
FOR r IN SELECT p.oid::regprocedure AS fn FROM pg_proc p
|
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
WHERE n.nspname = 'public' AND p.prokind = 'f' LOOP
|
|
EXECUTE format('ALTER FUNCTION %s OWNER TO bookhoard_srv', r.fn);
|
|
END LOOP;
|
|
END $$$$;
|
|
ALTER SCHEMA public OWNER TO bookhoard_srv;
|
|
ALTER DATABASE bookhoard OWNER TO bookhoard_srv;
|
|
EOSQL
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
restart: "no"
|
|
|
|
# Bookhoard Application
|
|
# In production this image is pulled from the Gitea container registry.
|
|
# Override IMAGE_TAG in .env to pin or rollback a specific version (defaults to "latest").
|
|
app:
|
|
image: git.linuxhg.com/bookhoard/bookhoard:${IMAGE_TAG:-latest}
|
|
container_name: bookhoard
|
|
restart: unless-stopped
|
|
environment:
|
|
# Database Configuration
|
|
# Runtime role = bookhoard_srv (non-superuser OWNER, subject to RLS
|
|
# via FORCE). POSTGRES_USER above is the bootstrap account ONLY —
|
|
# the app must NEVER connect as it (superusers bypass RLS).
|
|
DATABASE_HOST: db
|
|
DATABASE_PORT: ${DB_PORT:-5432}
|
|
DATABASE_USER: bookhoard_srv
|
|
DATABASE_PASSWORD: ${DBSRVPASS}
|
|
DATABASE_NAME: bookhoard
|
|
|
|
# Application Configuration
|
|
JWT_SECRET: ${JWT_SECRET}
|
|
SERVER_PORT: ${SERVER_PORT:-8765}
|
|
# IMPORTANT: Device sync requires full URL with protocol
|
|
# Local: http://localhost:8765
|
|
# Local network: http://192.168.1.X:8765
|
|
# Domain: https://bookhoard.example.com
|
|
BASE_URL: ${BASE_URL:-http://localhost:8765}
|
|
# Mark session cookies Secure; set true behind a TLS-terminating reverse proxy (Caddy/nginx/traefik)
|
|
COOKIE_SECURE: ${COOKIE_SECURE:-false}
|
|
|
|
# Rate Limiting Configuration
|
|
TEST_MODE: ${TEST_MODE:-false}
|
|
RATE_LIMIT_ENABLED: ${RATE_LIMIT_ENABLED:-true}
|
|
REQUESTS_PER_MINUTE: ${REQUESTS_PER_MINUTE:-10}
|
|
|
|
# Conversion Service Configuration
|
|
BOOKHOARD_CONVERSION_CACHE_DIR: ${BOOKHOARD_CONVERSION_CACHE_DIR:-/app/cache/kepub}
|
|
BOOKHOARD_CONVERSION_TOOL: ${BOOKHOARD_CONVERSION_TOOL:-/usr/bin/kepubify}
|
|
BOOKHOARD_CONVERSION_CACHE_TTL: ${BOOKHOARD_CONVERSION_CACHE_TTL:-24h}
|
|
|
|
# Library Maintenance
|
|
# Days an archived item (file missing from disk for 2+ scans) is kept,
|
|
# with its reading history, before library scans purge it for good.
|
|
# Set 0 to keep archived items until purged manually on the library admin page.
|
|
ARCHIVE_RETENTION_DAYS: ${ARCHIVE_RETENTION_DAYS:-90}
|
|
|
|
# System timezone (fallback for server-side time operations)
|
|
TZ: ${TZ:-UTC}
|
|
ports:
|
|
- "${SERVER_PORT:-8765}:${SERVER_PORT:-8765}"
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
db-init:
|
|
condition: service_completed_successfully
|
|
volumes:
|
|
- ./uploads:/app/uploads
|
|
- bookhoard_conversion_cache:/app/cache/kepub
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER_PORT:-8765}/health || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
|
|
# Named Volumes
|
|
volumes:
|
|
postgres_data:
|
|
bookhoard_conversion_cache:
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Project Status
|
|
|
|
**Version**: 1.0
|
|
**License**: AGPL-3.0
|
|
**Status**: Production-ready ✅
|
|
|
|
---
|
|
|
|
## 🤝 Contributing
|
|
|
|
We welcome contributions! Please see [docs/developer/development.md](docs/developer/development.md) for guidelines.
|
|
|
|
---
|
|
|
|
## 📄 License
|
|
|
|
AGPL-3.0 - See [LICENSE](LICENSE) file for details.
|
|
|
|
---
|
|
|
|
**Built with ❤️ using Go, PostgreSQL, HTMX, and Tailwind CSS**
|