Installation
three ways in.
Docker on a Linux server, the installer script with systemd, or a virtual environment by hand. Then the single-user first run, or SaaS mode. A reference of every command closes the page.
01Choose a way
| Way | When | Status |
|---|---|---|
Docker — docker compose up -d | A server, the fastest start | Verified build, start, health check, onboarding and check in the container |
deploy/install.sh + systemd | A server without Docker, or your own Linux computer (PC actions then work on your files) | Installer verified in a clean Debian container; units passed systemd-analyze verify but Unverified on a real server |
| By hand (venv + pip) | Development | Same steps as the installer |
02Docker
One image with Python 3.11 and Node 20, no key inside. It runs as user vox (uid 10001) with a read-only root file system; only app/data (the named volume vox-data) is writable.
cp deploy/vox.env.example deploy/vox.env && chmod 600 deploy/vox.env # optional: keys can also go in the console
docker compose up -d --build
docker compose logs vox | grep "VOX admin" # console URL with its token
docker compose run --rm vox node src/run.js # the 12 onboarding questions
docker compose run --rm vox python -m voice check # what is missing, no network, no key
Without a fixed VOX_ADMIN_TOKEN (16+ characters) in deploy/vox.env, a new random console token is printed to the container log at every start, where anyone who can read Docker logs sees it.
Other ports
VOX_ADMIN_PORT=9766 VOX_MEDIA_PORT=9765 docker compose up -d
These are Compose variables (defaults 8766 and 8765), not VOX settings. The health check moves with them.
What does not work in Docker
- PC actions and desktop clean-up work on the container's files, not on the user's computer. For that, use the user unit (below).
- MCP servers over
stdio(uvx,npx): those tools are not in the image. Use HTTP MCP servers or extend the image. - The spoken terminal briefing (
sayis macOS only). It is printed.
SaaS mode in Docker
The shipped compose file runs single-user mode. For SaaS, override the command and the health check, for example in docker-compose.override.yml:
services:
vox:
command: ["python", "-m", "voice", "saas", "--host", "127.0.0.1", "--port", "8080",
"--serve-host", "127.0.0.1", "--serve-port", "8765"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request as u; u.build_opener(u.ProxyHandler({})).open('http://127.0.0.1:8080/api/csrf', timeout=4)"]
docker compose run --rm vox python -m voice saas migrate
docker compose run --rm vox python -m voice saas create-operator --email you@example.com --role owner
This SaaS override is derived from the code; it has not been run as a container.
03Installer script (no Docker)
deploy/install.sh # options: --python python3.11 · --dev (also installs the test tools)
It checks Python ≥ 3.11 and Node ≥ 20, installs @anthropic-ai/sdk from the lock file (npm ci --omit=dev --ignore-scripts), creates app/.venv, installs app/voice/requirements.txt, installs and verifies NLTK punkt_tab, runs python -m voice check and prints the next steps. It never asks for, reads or stores a key. Running it again is safe.
04By hand
cd app
python3 -m venv .venv
. .venv/bin/activate
pip install -r voice/requirements.txt
python -m voice setup-nltk # sentence-splitter data; without it the bot is silent
python -m voice check
setup-nltk options: --dir D (install elsewhere, then set NLTK_DATA to that directory), --from-zip Z (offline, from a zip downloaded elsewhere; the zip is pinned by SHA-256), --force.
Tests (offline, no keys): pip install -r voice/requirements-dev.txt && python -m pytest -q.
05Service units (systemd)
| Unit | When |
|---|---|
deploy/systemd/mikodes-vox.service | A server: system user vox, code in /opt/mikodes-vox, keys in /etc/mikodes-vox/vox.env (0640) or in the console, hardened (ProtectSystem=strict, writes only app/data). |
deploy/systemd/mikodes-vox.user.service | Your own Linux computer with systemctl --user: runs as you, so PC actions work on your files. |
The install procedure is in the header of each file. Both run python -m voice up and send SIGINT on stop, which up treats like Ctrl+C. For SaaS mode change ExecStart to python -m voice saas --host 127.0.0.1 --port 8080 --serve-host 127.0.0.1 --serve-port 8765. The console URL with its token is in journalctl (not printed when VOX_ADMIN_TOKEN is set).
06First run: single-user
- Answer the 12 questions
node src/run.jsfromapp/. Options:--answers file.json(scripted),--no-speak. This step never calls anyone. - Start everything
python -m voice up. It prints one line,VOX admin http://127.0.0.1:8766/#token=…, once the console really listens. - Enter the keysOpen that URL, go to Setup and enter the keys (Configuration). The media server starts only when all seven required keys and the NLTK data are present, so restart
upafter entering them. - Verify your own numberProfile → Verify number (how). Until then even a call to yourself is treated as a third-party call.
- Place the first call
python -m voice dial --purpose "morning briefing", or the Dial section of the console.
07First run: SaaS mode
cd app && . .venv/bin/activate
python -m voice saas migrate # create / upgrade data/saas/vox.db
python -m voice saas create-operator --email you@example.com --role owner
python -m voice saas --host 127.0.0.1 --port 8080 --serve-host 127.0.0.1 --serve-port 8765
One process serves the public site (/), each customer's console (/app/) and the operator panel (/op/) on port 8080, runs the multi-tenant worker, and starts the media server on port 8765 when every required key is present. Ctrl+C stops everything. Put a TLS proxy in front (Deployment) and continue with Operator panel.
Option of python -m voice saas | Default | Meaning |
|---|---|---|
--host | 127.0.0.1 | address of the web app; publish it through the proxy |
--port | 8080 | port of the web app |
--insecure-http | off | cookies without the Secure flag and no HSTS. Only for development on your own machine. |
--no-worker / --no-serve | off | do not run the job worker / never start the media server |
--serve-host / --serve-port | 0.0.0.0 / 8765 | media server address; use 127.0.0.1 behind a proxy |
--interval / --mail-every | 5 s / 5 min | worker round and mail check interval |
--data-dir | app/data | the data directory (also for saas migrate and saas create-operator); VOX_DATA_DIR does the same |
The database is <data>/saas/vox.db (0600) and each customer lives in <data>/tenants/<tenant_id>/. Do not run up and saas on the same data directory at the same time.
08Command reference
| Command | What it does | Exit codes |
|---|---|---|
python -m voice check | profile, own number, rings, disclosure sentence, barge-in settings and every missing key, without network | 0 all set · 2 something missing |
python -m voice serve [--host] [--port 8765] | media server only | 2 missing keys |
python -m voice dial --purpose "…" [--to +…] [--ring self|third_party|cold] [--lang en|sk] | places a call; without --to it calls the number in the profile | 0 accepted · 1 refused by the provider · 2 keys / bad number / cold |
python -m voice admin [--host 127.0.0.1] [--port 8766] | console only | — |
python -m voice worker [--once] [--interval 5] [--mail-every 5] | job worker only | 0 |
python -m voice up [--no-serve] [--port 8766] [--admin-host] [--serve-host] [--serve-port] … | single-user mode: console + worker (+ media server) | 0 Ctrl+C · 1 a service stopped · 2 bad admin token |
python -m voice saas [options] | SaaS mode | as up |
python -m voice saas migrate | applies every migration in app/voice/saas/migrations/ in name order | 0 · 2 migration error |
python -m voice saas create-operator --email E [--name N] [--role owner|staff] [--password-stdin] | creates an operator account for /op/; the password is typed twice (or piped), never an argument, never printed | 0 · 2 |
python -m voice mail-auth [--no-browser] | single-user: connect Gmail once (read-only) | 2 Google libraries missing |
python -m voice calendar-auth [--allow-create] [--no-browser] | single-user: connect Google Calendar; read-only unless --allow-create | 2 Google libraries missing |
python -m voice setup-nltk [--dir D] [--from-zip Z] [--force] | installs NLTK punkt_tab | — |
python -m voice local-check [--lang en|sk] | Piper speaks a test sentence, whisper.cpp transcribes it back (only the local engines you chose) | 0 works / none chosen · 2 an engine fails |
python -m voice wakeword-test FILE.wav | runs your own openWakeWord model over a 16 kHz mono recording | 0 ran · 2 off / missing |
serve, admin, worker, up and saas also take --data-dir PATH. --help works on every command.