# Maven — Docker deployment One image, one container per daemon (`docker-compose.yml`). Core (`mavend`) holds the encryption key and the db; the modules mount only the shared socket dir and read-only models. ## First run ```sh # 1. generate the at-rest db key (32 bytes, base64) — keep it safe, losing it loses the db cp deploy/db_key.env.example deploy/db_key.env printf 'MAVEN_DB_KEY=%s\n' "$(openssl rand 32 | base64 -w0)" > deploy/db_key.env # 2. build + start docker compose build docker compose up -d # 3. logs docker compose logs -f mavend ``` `models/` and `deps/` are bind-mounted / baked from the host — they are NOT in git (fetched via `make deps` + downloaded models). The build context needs `deps/lib`, `deps/piper`, `deps/include`, and `deps/whisper.cpp/ggml/include` present (see `.dockerignore`). ## Layout | Path (in container) | What | |----------------------------|-----------------------------------------| | `/opt/maven/bin` | the six daemons | | `/opt/maven/lib` | native .so (whisper+vulkan, onnxruntime)| | `/opt/maven/piper` | piper binary + espeak data | | `/opt/maven/models` (ro) | bind-mount of `./models` | | `/run/maven` (volume) | shared IPC sockets | | `/var/lib/maven` (volume) | encrypted db at rest | | `/dev/shm` (tmpfs) | decrypted db working copy (RAM only) | ## Reading the outside world (off by default) `mavend.json` ships without a `feeds` block, which means no RSS/Atom feed is fetched and no outbound request is made. Switching it on is adding the block: ```json "feeds": { "poll_interval": "30m", "max_items": 5, "max_age": "24h", "sources": [ { "name": "habr", "url": "https://habr.com/ru/rss/best/daily/", "category": "технологии", "exclude": ["реклама"] } ] } ``` What it does and does not do: - items are written as notes with source `rss:`, visible on `/dash`; - **nothing is announced.** She reads them back when asked — "что нового в лентах?", "что нового по технологиям?" — and never on arrival. There is no severity or channel knob here on purpose; - the fetcher is allowlisted to the hosts of the configured feeds, plus any `allow_hosts`. It refuses non-http(s) schemes and every private address (loopback, the LAN, the `10.42.0.0/24` wg range, cloud metadata). It caps the response at 2 MiB and redirects at 3, and makes at most one request per host per second. See `internal/webfetch`; - how far each feed was read is stored as a config fact `rss:latest:`, so a restart does not re-note yesterday's headlines. ### Reading a page (`crawl`, also off by default) There is no `crawl` block either, so no page is fetched. Two halves, separately switched: ```json "crawl": { "on_demand": true, "interval": "6h", "max_runes": 4000, "watches": [ { "name": "changelog", "url": "https://example.org/changelog", "interval": "12h" } ] } ``` - `on_demand` lets her read a page he names in the utterance: "посмотри https://example.org/x — что там?". The page becomes context for his question, and only the URL leaves the box. Without a URL nothing is fetched, so this is a fallback and not a habit; - `watches` re-reads a fixed list on its interval and writes a note when the text changed. Like the feeds, it announces nothing; - the answer path sits **last** in the query chain, behind his memory, his notes and (once wired) the local Kiwix ZIMs. A local read costs nothing; - `robots.txt` is fetched first and obeyed with no override; a `Disallow` is a refusal she says out loud. `Crawl-delay` is honoured; - same guarded fetcher as the feeds: allowlist/denylist, no private addresses, size cap, redirect cap, timeout, one request per host per second; - dedup state is the config fact `crawl:hash:`. ## Not yet verified / host-dependent This stack is correct-by-construction but has **not been build-tested here** (no docker in the authoring env; ~1GB context; GPU). Expect a tweak on first build on the target host, most likely in one of these: - **GPU passthrough** — `mavsttd` maps `/dev/dri` for Vulkan. On an NVIDIA host you'd swap to the nvidia container runtime instead of `/dev/dri`. - **onnxruntime lib path** — `mavend`'s embedder needs `libonnxruntime.so` (on `LD_LIBRARY_PATH=/opt/maven/lib`). If the embedder wants an explicit path, set it in the config's embedder block. - **cross-container voice** — `mavweb -voice mavend:9100` only works once `mavend` binds its voice server on `0.0.0.0:9100` (Voice config, currently unset). Until then, voice-over-web is inert; `/tools`, passkey, and the dash work fine over the core socket. - **netdata** — `mavpoll` reaches it via `host.docker.internal`; adjust if netdata runs elsewhere. ## Updating her (`mavupdate`, Vikunja #249) Off unless configured, and there is deliberately no button for it. There is no IPC method, no web route, no timer and no act that starts an update — the trigger is a human running `mavupdate` on the host, which needs shell access, a strictly higher bar than the step-up passkey gate that guards `/tools`. She cannot update herself; she can be updated. Nothing here ever fetches code: the new version is whatever you pulled into the working tree yourself. Add an `update` block to `mavend.json` (mavend ignores it — only the CLI reads it), with paths as they exist **on the host**, not inside a container: ```json "update": { "source_dir": "/home/kami/apps/Maven", "install_dir": "/home/kami/apps/Maven", "snapshot_dir": "/var/lib/maven-snapshots", "binaries": ["mavend", "mavweb", "mavsttd", "mavttsd", "mavwaked", "mavenclient", "mavpoll", "mavcaldav", "mavmaild"], "config_files": ["deploy/mavend.json"], "restart_cmd": ["docker", "compose", "up", "-d", "--build"], "health_socket": "/var/lib/docker/volumes/maven_sockets/_data/mavend.sock", "health_timeout_sec": 120 } ``` `snapshot_dir` must be outside `install_dir` (a restore must not read from what the install writes) and `health_socket` is required: an update that cannot check its own result cannot roll itself back, so the config is refused without one. Then: ```sh mavupdate -config deploy/mavend.json verify # make build + make test, deploys nothing mavupdate -config deploy/mavend.json apply -yes # snapshot, verify, install, restart, health-check mavupdate -config deploy/mavend.json list # what you can roll back to mavupdate -config deploy/mavend.json rollback -yes # restore the previous artifacts and restart ``` `apply` refuses to start if she is not already answering — otherwise a failed update and a box that was already broken are indistinguishable afterwards. On any failure after the install it restores the snapshot, restarts, and checks again; if that also fails it says so loudly and names the directory to copy back by hand. The database is never snapshotted or rolled back (see the package comment in `internal/update`); schema compatibility is `store.Migrate`'s job.