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.
5.2 KiB
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 composeordocker 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:
# --- 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:
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.
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:podman exec vellum \ sqlite3 /data/vellum.db "VACUUM INTO '/data/backup.db'"VACUUM INTOwrites 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
- Back up first (see above). Migrations are one-way.
- Pull or rebuild the image, then recreate the container:
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):
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.