Vellum/DEPLOY.md
mithral f680e1d9f6 Beta blockers: async scans, admin CLI, deploy docs, pagination, password UI
Five tasks, four delegated to agents on disjoint files and one on the critical
path. Board and verification log: ~/.opencode/plan/vellum-beta-tasks.md

T1 Async scans (coordinator)
- migration 0007 adds ref_id + result to jobs, indexed by (kind, ref_id, id),
  so a job can be found by what it operates on and can report back.
- jobs: EnqueueRef, JobID(ctx) so a handler can attach a result, SetResult.
- POST /libraries/{id}/scan now enqueues and returns 202; a real library takes
  longer than any request timeout, and a hanging request looks like a broken
  server. GET /libraries/{id}/scan/status reports the latest run.
- Libraries.svelte polls and shows "Scanning…" instead of freezing.

T2 Admin CLI (agent)
- internal/cli: `user add|passwd|list`, `backup <path>` via VACUUM INTO, with
  tests. Wired into main.go, which the coordinator kept.

T3 Deploy (agent)
- compose.yml now mounts all six media kinds read-only with :z and carries the
  metadata env keys. DEPLOY.md covers volumes, SELinux, admin bootstrap, TLS,
  backup and upgrade.

T4 Pagination (agent)
- GET /items takes offset and returns total; Browse loads 60 at a time with
  "Load more" and "Showing X of Y" instead of silently truncating at 300.

T5 Password change UI (agent)
- A Password section in Settings with validation, busy and success states.

Verified end to end: scan returns 202 in 0.7 ms and polls to done; the CLI
created a user, changed a password, and produced a complete 240 KB backup while
the server ran; pagination reports total 8 and pages correctly; compose parses
with every mount :z. go vet and 14 test packages pass; svelte-check 0/0.
2026-10-10 18:00:35 -04:00

145 lines
5.2 KiB
Markdown

# Deploying Vellum
Vellum is a single Go binary in a single container with a single SQLite
database. This document covers a container deployment with `compose.yml`.
## Prerequisites
- **Podman or Docker** with the Compose plugin (`podman compose` or
`docker compose`).
- **A media folder** on the host, laid out one directory per media kind
(`ebooks/`, `comics/`, `audiobooks/`, `music/`, `photos/`, `games/`).
Vellum scans these; it never writes to them.
- A host directory for the database and cache (e.g.
`/opt/mediaserver/vellum/{data,config}`).
The image is built locally with `make docker` (Podman) or
`docker build -t localhost/vellum:0.1.0-dev .`.
## Configuration: `.env`
Every setting is an environment variable with a working default, so Vellum
starts with no configuration at all. Copy the relevant values into a `.env`
next to `compose.yml`:
```dotenv
# --- Media roots (colon-separated, read-only) -------------------------------
VELLUM_MEDIA_DIRS=/media/ebooks:/media/comics:/media/audiobooks:/media/music:/media/photos:/media/games
# --- Server -----------------------------------------------------------------
VELLUM_LISTEN=:7788
VELLUM_LOG_LEVEL=info
# --- First admin (only used on an EMPTY database) ---------------------------
VELLUM_ADMIN_USER=admin
VELLUM_ADMIN_PASSWORD=change-me-at-least-8-chars
# --- Optional metadata providers --------------------------------------------
VELLUM_COMICVINE_API_KEY=
VELLUM_IGDB_CLIENT_ID=
VELLUM_IGDB_CLIENT_SECRET=
# --- Set to true once you serve over TLS ------------------------------------
VELLUM_COOKIE_SECURE=false
```
The same names are what the process reads directly; `compose.yml` passes them
through. Defaults, for reference: `VELLUM_LISTEN=:7788`,
`VELLUM_LOG_LEVEL=info`, `VELLUM_COOKIE_SECURE=false`.
## Volumes and SELinux
`compose.yml` uses three kinds of mount:
| Mount | Mode | Purpose |
| --- | --- | --- |
| `/data` | read/write | SQLite database (`vellum.db`) and persistent state |
| `/config` | read/write | Regenerable cache: covers, thumbnails, extracted pages |
| `/media/<kind>` | **read-only** | The media library |
Each media kind is bind-mounted individually and read-only. **Vellum never
writes to `/media`.** If you add a media root, add a matching read-only mount
and a matching entry in `VELLUM_MEDIA_DIRS`.
Every bind mount carries the `:z` suffix, which is **mandatory on SELinux
hosts** (this stack runs with `getenforce = Enforcing`). Without `:z` the
container cannot read the host paths and Vellum fails to start with permission
errors. `:z` relabels the files so the container's SELinux context may access
them; `:ro` keeps the media read-only.
## First admin bootstrap
Set `VELLUM_ADMIN_USER` and `VELLUM_ADMIN_PASSWORD` (minimum 8 characters)
before the first start. On boot, if the users table is empty, Vellum creates
that administrator.
This happens **only on an empty database**. Setting these on an established
install does nothing — the credentials are ignored. If the database is empty
and both are unset, the server logs a warning and refuses to invent a default
password. Once your admin exists, remove the two variables from `.env`.
## Reverse proxy and TLS
Vellum speaks plain HTTP on `:7788`; `compose.yml` binds it to
`127.0.0.1:7788` so it is only reachable from the host. Put a reverse proxy
(nginx, Caddy, Traefik) in front of it to terminate TLS and forward to
`http://127.0.0.1:7788`.
Once traffic arrives over HTTPS, set:
```dotenv
VELLUM_COOKIE_SECURE=true
```
This marks session cookies `Secure`, so browsers only send them over TLS. When
it is `false` Vellum logs a warning at boot because session cookies can be sent
over plain HTTP. Restart the container after changing it.
## Backup
The entire persistent state is the SQLite database at `/data/vellum.db` (in
WAL mode). Two safe options:
- **Stop the container, then copy the database.**
```bash
podman compose stop vellum
cp /opt/mediaserver/vellum/data/vellum.db /backup/vellum-$(date +%F).db
podman compose start vellum
```
- **Take a consistent snapshot while running** with SQLite's `VACUUM INTO`:
```bash
podman exec vellum \
sqlite3 /data/vellum.db "VACUUM INTO '/data/backup.db'"
```
`VACUUM INTO` writes a single consistent file without stopping the server;
copy that file off the volume when it is done.
`/config` holds only regenerable cache and does not need backing up.
## Upgrading
1. **Back up first** (see above). Migrations are one-way.
2. Pull or rebuild the image, then recreate the container:
```bash
make docker
podman compose up -d
```
**Migrations run automatically at boot.** They are embedded and applied in
filename order, recorded by number in `PRAGMA user_version`. Never edit an
applied migration; a new schema change is always a new file. If a migration
fails, the process exits and the container restarts — restore your backup.
## Logs
Vellum logs to **stdout/stderr**, so container logs are the log. With the
`json-file` driver configured in `compose.yml` (10 MB per file, 3 files):
```bash
podman logs -f vellum # follow
podman logs --tail 100 vellum # last 100 lines
```
Verbosity is controlled by `VELLUM_LOG_LEVEL` (`debug`, `info`, `warn`,
`error`). Set `VELLUM_LOG_FORMAT=json` for structured output if you ship logs
to a collector.