docs(readme): prod install without a repo clone — copyable compose
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).
This commit is contained in:
1 parent
efbe312f16
commit
de26544d9d
1 file changed
+125
-15
@@ -21,30 +21,140 @@ A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and T
|
|||||||
- **Podman** (recommended) or Docker
|
- **Podman** (recommended) or Docker
|
||||||
- **5 minutes** of your time
|
- **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
|
```bash
|
||||||
# 1. Clone the repository
|
mkdir bookhoard && cd bookhoard
|
||||||
git clone https://git.linuxhg.com/Bookhoard/bookhoard.git
|
mkdir uploads
|
||||||
cd bookhoard
|
```
|
||||||
|
|
||||||
# 2. Set up environment
|
**`docker-compose.yml`** — copy this wholesale:
|
||||||
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
|
|
||||||
|
|
||||||
# 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 pull
|
||||||
docker compose up -d
|
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
|
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`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user