# 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**