From de26544d9dbb6aa0324d2f47fa3181954782bd3f Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 29 Oct 2026 12:05:56 -0400 Subject: [PATCH] =?UTF-8?q?docs(readme):=20prod=20install=20without=20a=20?= =?UTF-8?q?repo=20clone=20=E2=80=94=20copyable=20compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Installation section assumed 'git clone the repo'. Self-hosted operators only need three files. The install is now: mkdir + copy this compose block + .env (three openssl secrets) + uploads dir + pull/up. The compose is fully self-contained: the runtime-role bootstrap is inlined into db-init's command (same SQL as database/schema/z_rls_role.sh — keep in sync) — no ./database/schema bind mount, because the app itself executes the schema as the runtime role (owner) on first boot; with the role as DATABASE owner the trusted pg_trgm extension needs no superuser. Verified end to end in a throwaway project: fresh volume, pasted block, .env with generated secrets, db-init converged role+ownership, app ran the full schema and came up healthy — no repo checkout anywhere. Compose-quoting lessons encoded (the reason for odd dollars): compose interpolates '532016' to '$' before the container sees it, so the plpgsql 532016 tags in the inline SQL are written 532016532016, and PGUSER must be set explicitly (POSTGRES_USER is not a libpq variable). --- README.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b3276f2c..53d10de1 100644 --- a/README.md +++ b/README.md @@ -21,30 +21,140 @@ A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and T - **Podman** (recommended) or Docker - **5 minutes** of your time -### Installation +### 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 -# 1. Clone the repository -git clone https://git.linuxhg.com/Bookhoard/bookhoard.git -cd bookhoard +mkdir bookhoard && cd bookhoard +mkdir uploads +``` -# 2. Set up environment -cp .env.example .env -# Generate secure passwords (no special characters): -# JWT_SECRET: openssl rand -hex 32 -# DBPASS: openssl rand -hex 16 -# Edit .env with your generated values +**`docker-compose.yml`** — copy this wholesale: -# 3. Pull images and start the server +```yaml +services: + db: + image: postgres:15-alpine + container_name: bookhoard_db + environment: + POSTGRES_DB: bookhoard + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${DBPASS} + PGPORT: 5432 + volumes: + - postgres_data:/var/lib/postgresql/data + - ./uploads:/app/uploads + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + db-init: + image: postgres:15-alpine + container_name: bookhoard_db_init + environment: + POSTGRES_USER: postgres + POSTGRES_DB: bookhoard + PGHOST: db + PGUSER: postgres + 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" + + app: + image: git.linuxhg.com/bookhoard/bookhoard:${IMAGE_TAG:-latest} + container_name: bookhoard + restart: unless-stopped + environment: + DATABASE_HOST: db + DATABASE_PORT: 5432 + # Runtime role: bookhoard_srv (created above, confirmed by db-init). + # NEVER connect the app as the bootstrap superuser — it is subject + # to row-level security (per-user library visibility) by design. + DATABASE_USER: bookhoard_srv + DATABASE_PASSWORD: ${DBSRVPASS} + JWT_SECRET: ${JWT_SECRET} + SERVER_PORT: 8765 + ports: + - "8765:8765" + depends_on: + db: + condition: service_healthy + db-init: + condition: service_completed_successfully + volumes: + - ./uploads:/app/uploads + - bookhoard_conversion_cache:/app/cache/kepub + +volumes: + postgres_data: + bookhoard_conversion_cache: +``` + +**`.env`** — create alongside the compose file and fill in the three +secrets (no special characters): + +```bash +openssl rand -hex 32 # JWT_SECRET +openssl rand -hex 16 # DBPASS (bootstrap database password) +openssl rand -hex 16 # DBSRVPASS (runtime role password) +``` + +Then start: + +```bash docker compose pull docker compose up -d -# Optionally pin a specific version: set IMAGE_TAG in .env (defaults to "latest") - -# 4. Open your browser open http://localhost:8765 ``` -The first user to register automatically becomes an admin. +Notes: +- `db-init` runs on every startup and converges the runtime role + (idempotent, ~1 second); the app refuses to start before it succeeds. +- `uploads/` on the host is where your book files live; libraries scan + folders under it from the Admin → Libraries page. +- First user to register automatically becomes an admin. +- Upgrades: `docker compose pull && docker compose up -d` (pin a version + with `IMAGE_TAG` in `.env`). ---