deploy: dockerize — one image, one container per daemon
Compose stack replacing start-maven.sh's bare `&`-backgrounded processes. Single multi-stage image builds all six daemons (CGO + prebuilt native libs from deps/); compose runs one container each with a different command. Only mavend mounts the encryption key (env_file, gitignored) and the db volume; the modules mount just the shared unix-socket dir and read-only models — so the "key-free modules" boundary is OS-enforced (separate namespaces), not just a code convention. IPC stays unix-domain over a shared volume: zero code change, paths move to /run/maven. Encrypted db at rest on a named volume, decrypted working copy in tmpfs (RAM) per the at-rest encryption landed earlier. Validated: `docker compose config` clean, mavend.json parses, all daemon flags confirmed. NOT build-tested (no docker/GPU in authoring env) — deploy/README.md lists the host-dependent tweak points (GPU passthrough, onnxruntime path, cross-container voice bind, netdata host). Chosen Docker over interim systemd units per the "dockerize soon" call — no throwaway supervisor built. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# keep the build context small — the repo carries a full Go toolchain, the
|
||||||
|
# whisper.cpp source tree, and ~1GB of models that must NOT ship in the image.
|
||||||
|
.git
|
||||||
|
models
|
||||||
|
*.db
|
||||||
|
certs
|
||||||
|
|
||||||
|
# built binaries (rebuilt inside the image)
|
||||||
|
/mavend
|
||||||
|
/mavsttd
|
||||||
|
/mavttsd
|
||||||
|
/mavweb
|
||||||
|
/mavpoll
|
||||||
|
/mavcaldav
|
||||||
|
/mavenclient
|
||||||
|
|
||||||
|
# heavy deps we don't need in context. We keep only the prebuilt runtime libs
|
||||||
|
# (deps/lib, deps/piper) and the headers the CGO build needs.
|
||||||
|
deps/go
|
||||||
|
deps/onnxruntime-linux-x64-*
|
||||||
|
deps/whisper.cpp/**
|
||||||
|
!deps/whisper.cpp
|
||||||
|
!deps/whisper.cpp/ggml
|
||||||
|
!deps/whisper.cpp/ggml/include
|
||||||
|
!deps/whisper.cpp/ggml/include/**
|
||||||
|
|
||||||
|
# local secrets — never bake into an image layer
|
||||||
|
deploy/db_key.env
|
||||||
@@ -19,6 +19,9 @@ models/
|
|||||||
# Runtime data
|
# Runtime data
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
# Deploy secret (the at-rest db key) — never commit
|
||||||
|
deploy/db_key.env
|
||||||
|
|
||||||
# Temp files
|
# Temp files
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# Single image, all six daemons. docker-compose runs one container per daemon
|
||||||
|
# off this image with a different command — the native-lib + toolchain surface
|
||||||
|
# is shared, so separate images would duplicate ~all of it. Isolation still
|
||||||
|
# holds: each daemon is its own container/namespace, only mavend mounts the key
|
||||||
|
# and the db volume.
|
||||||
|
#
|
||||||
|
# Native deps are the prebuilt artifacts the repo already carries under deps/
|
||||||
|
# (libwhisper+ggml-vulkan, onnxruntime, piper/espeak). We do NOT build
|
||||||
|
# whisper.cpp from source here — COPY the prebuilt .so and headers.
|
||||||
|
# ponytail: prebuilt-lib copy, not a from-source build. Add a whisper.cpp build
|
||||||
|
# stage if you ever need reproducibility / a different arch than the host libs.
|
||||||
|
|
||||||
|
FROM golang:1.23-bookworm AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# native build inputs (prebuilt libs + headers), then module cache, then source
|
||||||
|
COPY deps/lib/ /src/deps/lib/
|
||||||
|
COPY deps/piper/ /src/deps/piper/
|
||||||
|
COPY deps/include/ /src/deps/include/
|
||||||
|
COPY deps/whisper.cpp/ggml/include/ /src/deps/whisper.cpp/ggml/include/
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY cmd/ ./cmd/
|
||||||
|
COPY internal/ ./internal/
|
||||||
|
|
||||||
|
# CGO wiring mirrors the Makefile; rpath points at the RUNTIME lib location so
|
||||||
|
# the binaries find their .so at /opt/maven/lib regardless of LD_LIBRARY_PATH.
|
||||||
|
ENV CGO_ENABLED=1 \
|
||||||
|
CGO_CFLAGS="-I/src/deps/include -I/src/deps/whisper.cpp/ggml/include" \
|
||||||
|
CGO_LDFLAGS="-L/src/deps/lib -L/src/deps/piper -Wl,-rpath,/opt/maven/lib"
|
||||||
|
RUN go build -o /out/mavend ./cmd/mavend && \
|
||||||
|
go build -o /out/mavsttd ./cmd/mavsttd && \
|
||||||
|
go build -o /out/mavttsd ./cmd/mavttsd && \
|
||||||
|
go build -o /out/mavweb ./cmd/mavweb && \
|
||||||
|
go build -o /out/mavpoll ./cmd/mavpoll && \
|
||||||
|
go build -o /out/mavcaldav ./cmd/mavcaldav
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates libvulkan1 mesa-vulkan-drivers libgomp1 && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# runtime native libs: whisper/ggml (incl. vulkan), onnxruntime, piper/espeak.
|
||||||
|
COPY deps/lib/ /opt/maven/lib/
|
||||||
|
COPY deps/piper/ /opt/maven/piper/
|
||||||
|
# piper ships its own .so (onnxruntime, espeak, phonemize) — put them on the path too.
|
||||||
|
RUN cp -a /opt/maven/piper/*.so* /opt/maven/lib/ 2>/dev/null || true
|
||||||
|
COPY --from=build /out/ /opt/maven/bin/
|
||||||
|
|
||||||
|
ENV LD_LIBRARY_PATH=/opt/maven/lib PATH=/opt/maven/bin:$PATH
|
||||||
|
|
||||||
|
# unprivileged; core owns the key + db, modules own nothing.
|
||||||
|
RUN useradd -r -u 10001 -m maven \
|
||||||
|
&& mkdir -p /run/maven /var/lib/maven \
|
||||||
|
&& chown maven:maven /run/maven /var/lib/maven
|
||||||
|
USER maven
|
||||||
|
WORKDIR /opt/maven
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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) |
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Copy to deploy/db_key.env (gitignored) and fill with a real key:
|
||||||
|
# openssl rand 32 | base64 -w0
|
||||||
|
# This is the AES-256 key that encrypts the at-rest db. Losing it = losing the db.
|
||||||
|
MAVEN_DB_KEY=
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"db_path": "/var/lib/maven/maven.db.enc",
|
||||||
|
"db_tmpfs": "/dev/shm/maven-plain.db",
|
||||||
|
"db_key_env": "MAVEN_DB_KEY",
|
||||||
|
"socket_path": "/run/maven/mavend.sock",
|
||||||
|
"state_dir": "/var/lib/maven"
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: maven
|
||||||
|
|
||||||
|
# One image (built once), one container per daemon. Only mavend holds the key
|
||||||
|
# and the db volume; the modules mount just the shared socket dir + models.
|
||||||
|
# IPC stays unix-domain over the shared `sockets` volume — no code change from
|
||||||
|
# the bare-metal setup, only paths move to /run/maven.
|
||||||
|
|
||||||
|
x-image: &image
|
||||||
|
image: maven:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
services:
|
||||||
|
mavend:
|
||||||
|
<<: *image
|
||||||
|
build: .
|
||||||
|
command: ["mavend", "-config", "/opt/maven/config/mavend.json"]
|
||||||
|
# the key lives ONLY here. deploy/db_key.env holds MAVEN_DB_KEY=<base64-32B>.
|
||||||
|
env_file: [./deploy/db_key.env]
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/maven # encrypted db at rest
|
||||||
|
- sockets:/run/maven # IPC socket dir
|
||||||
|
- ./deploy/mavend.json:/opt/maven/config/mavend.json:ro
|
||||||
|
- ./models:/opt/maven/models:ro
|
||||||
|
# the decrypted working copy lives in RAM (see db_tmpfs in mavend.json).
|
||||||
|
tmpfs:
|
||||||
|
- /dev/shm
|
||||||
|
|
||||||
|
mavsttd:
|
||||||
|
<<: *image
|
||||||
|
command: ["mavsttd", "-socket", "/run/maven/stt.sock", "-model", "/opt/maven/models/stt/ggml-small.bin"]
|
||||||
|
depends_on: [mavend]
|
||||||
|
# whisper uses libggml-vulkan → needs the GPU render node.
|
||||||
|
devices:
|
||||||
|
- "/dev/dri:/dev/dri"
|
||||||
|
volumes:
|
||||||
|
- sockets:/run/maven
|
||||||
|
- ./models:/opt/maven/models:ro
|
||||||
|
|
||||||
|
mavttsd:
|
||||||
|
<<: *image
|
||||||
|
command: ["mavttsd", "-socket", "/run/maven/tts.sock",
|
||||||
|
"-piper", "/opt/maven/piper/piper",
|
||||||
|
"-model", "/opt/maven/models/tts/ru_RU-irina-medium.onnx",
|
||||||
|
"-espeak_data", "/opt/maven/piper/espeak-ng-data"]
|
||||||
|
depends_on: [mavend]
|
||||||
|
volumes:
|
||||||
|
- sockets:/run/maven
|
||||||
|
- ./models:/opt/maven/models:ro
|
||||||
|
|
||||||
|
mavweb:
|
||||||
|
<<: *image
|
||||||
|
# NOTE: -voice must reach mavend's voice TCP server cross-container. That
|
||||||
|
# requires mavend to BIND its voice server on 0.0.0.0:9100 (Voice config,
|
||||||
|
# currently unset). Until that's configured, voice-over-web is inert — the
|
||||||
|
# rest of mavweb (/tools, passkey, dash) works over the core socket.
|
||||||
|
command: ["mavweb", "-addr", ":9201", "-voice", "mavend:9100", "-core", "/run/maven/mavend.sock"]
|
||||||
|
depends_on: [mavend]
|
||||||
|
ports: ["9201:9201"]
|
||||||
|
volumes:
|
||||||
|
- sockets:/run/maven
|
||||||
|
|
||||||
|
mavpoll:
|
||||||
|
<<: *image
|
||||||
|
command: ["mavpoll", "-socket", "/run/maven/mavend.sock", "-netdata", "http://host.docker.internal:19999"]
|
||||||
|
depends_on: [mavend]
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway" # reach netdata on the host
|
||||||
|
volumes:
|
||||||
|
- sockets:/run/maven
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
sockets:
|
||||||
Reference in New Issue
Block a user