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.
145 lines
5.2 KiB
Markdown
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.
|