Quick start (Docker Compose)
Prerequisites: a Linux host (Ubuntu 24.04) for production, Docker engine ≥ v26 (agent workers mount each granted workspace as an isolated volume subpath — older engines fail worker creation;make all checks and refuses), git, curl. A Mac with Docker Desktop works for a local
evaluation — everything runs in containers either way. Published vexaai/v012-* images include
linux/arm64 variants alongside linux/amd64, so Docker on Apple Silicon pulls arm64 where
available — but the arm64 images are published best-effort: release CI currently validates
only the amd64 images (no arm64 execution leg yet), so treat Apple Silicon as experimental.
vexaai/vexa-bot remains amd64-only (the install pulls the published image; make bot builds a local vexa/vexa-bot:dev for development). For GPU transcription on
Mac, point TRANSCRIPTION_SERVICE_URL at any OpenAI-compatible local endpoint
(see Configuration).
make all seeds .env from .env.example, brings the stack up, and prints an API key plus the
service URLs when it’s done. The meeting bot is built from source (make bot), not pulled —
the published vexaai/vexa-bot:dev on Docker Hub is the older 0.10 line and is not compatible with
this stack’s lifecycle.v1 (bots reach joining then fail). make all warns loudly if the bot image
is missing. For a transcript, set a transcription (STT) token in .env
(TRANSCRIPTION_SERVICE_TOKEN) — get one at vexa.ai/account, or self-host the transcription service on
a GPU so the audio path never calls out. The API is then at http://localhost:18056 (the gateway) and the
terminal web workbench at http://localhost:13000.
Don’t have a key yet? Hosted: sign in at vexa.ai/signin with a Google
account and copy your key from your account page — free credit, no card
required. Self-hosted: make all prints a key when the stack comes up.
The stack
The bot is not a long-running service — the runtime spawns a browser container per
meeting (
BROWSER_IMAGE) and an agent container per dispatch (AGENT_IMAGE), then reaps them. The
BROWSER_IMAGE is built from source here (make bot) and the runtime spawns it without pulling
— so it must exist locally before any bot can join (build it once; make all checks and warns if it’s
absent).
Configuration
- Transcription (STT) —
TRANSCRIPTION_SERVICE_URL/TRANSCRIPTION_SERVICE_TOKEN. Unset → defaultPOST /botsanswers 503 (refuse loud). Capture-only: setTRANSCRIBE_ENABLED=falseor pass{"transcribe_enabled": false}on the spawn. See Configuration. - Object storage — MinIO (
MINIO_*): meeting recordings and agent workspaces live in your bucket. The defaultMINIO_HOST_PORT=9000is a common port — if it’s already taken on your host (make allfails withbind … 127.0.0.1:9000 … address already in use), set a free port in.env. - Agent inference — bring your own: point the agent at your endpoint so no inference leaves the
network (
VEXA_AGENT_MODEL/ mounted credentials). - Secrets —
ADMIN_TOKEN,INTERNAL_API_SECRET, DB credentials. Set real values before exposing.
Transcription (the separate GPU unit)
Speech-to-text is the one GPU workload, so it is carved out of the main stack:make all
runs GPU-free and anywhere, and the STT service is its own deploy unit at
deploy/transcription
(core/meetings/services/transcription
is the brick — faster-whisper / CTranslate2 behind an OpenAI-compatible /v1/audio/transcriptions).
Language is detected per transcription window and stamped on each segment; force a single language
via the bot request’s language (see Send a bot).
Stand it up wherever a GPU lives (the same host or a dedicated GPU box):
deploy/compose/.env:
TRANSCRIPTION_MODEL as the OpenAI-compatible
model field on every request (unset → whisper-1). The bundled unit ignores it — pick its
model with the unit’s own MODEL_SIZE. Backends that validate the field need the right id:
Groq → whisper-large-v3-turbo, OpenAI → whisper-1 / gpt-4o-transcribe,
vLLM/LiteLLM → the exact served model name. Any OpenAI-compatible
/v1/audio/transcriptions endpoint works: set TRANSCRIPTION_SERVICE_URL to its base URL,
TRANSCRIPTION_SERVICE_TOKEN to its key, and TRANSCRIPTION_MODEL to the id it expects.
Now bots transcribe end-to-end: bot → transcription service → segments → meeting-api collector
→ live fan-out. Scale by adding workers (one GPU each) in the unit’s docker-compose.yml +
nginx.conf.
Publishing behind a reverse proxy
make all binds every service to 127.0.0.1 (loopback only). To expose the terminal at a public
hostname, put a TLS-terminating reverse proxy in front of the terminal port (TERMINAL_PORT, default
13000) and tell the terminal its public origin so auth cookies and OAuth callbacks are correct:
/ws to the gateway itself, so the proxy only needs standard
WebSocket-upgrade headers):
:18056) through a reverse proxy, set GUARD_TRUSTED_PROXIES to the
proxy’s IP and have the proxy forward the real client IP. The edge guard is on by default, so this
applies unless you have explicitly opted out:
X-Forwarded-For chain only from an
IP named in GUARD_TRUSTED_PROXIES (it reads the rightmost entry); a spoofed header from any other
source is ignored, so it cannot rotate an abuser’s budget. See
Configuration → Gateway edge protection.
The terminal carries its own Google/Microsoft OAuth login, so the proxy needs no auth of its own.
No egress
Everything runs inside your perimeter: gateway + services + redis/postgres/minio on your host, the transcription unit on your own GPU or CPU, BYO inference, recordings in your object storage. A bundled STT unit means the audio path never calls out — the posture the regulated verticals ask for. We have not run an install on a genuinely disconnected network, so we do not claim one: what a procurement review can hold us to is self-hosted with no egress, not a certified air gap. On Kubernetes the chart does not yet exposeTRANSCRIPTION_MODEL as a value — see
No-egress clusters.