Host roles
The platform is deployed by role, not by machine. A small deployment co-locates several roles on one host; a busy one splits them out and adds more fleet hosts. The roles, and what runs on each, are:| Role | What runs on it | systemd / process |
|---|---|---|
| API / App host | Backend API, Console static build (via nginx), MongoDB, Redis | voxbridge.service (uvicorn :8080), nginx |
| Fleet host(s) | N single-worker Voice fleet processes behind nginx, local MinIO | voxcore@1…N.service, nginx, MinIO |
| SIP / LiveKit host | LiveKit + livekit-sip, the single Dialler | docker-compose, voxdialler.service |
| WSS ingress host (optional) | HAProxy fronting multiple fleet hosts | HAProxy |
The Ori repo names map to the service names once, here, and the friendly names are used
everywhere else: Backend API =
vox-backend, Console = vox-frontend, Voice fleet =
vox-agents, Dialler = vox-dialler. All four live in the oriserve1 Bitbucket
workspace. See the repository map for the full breakdown.How Jenkins deploys to a host
Every role follows the same shape once the build is green: Jenkins reaches the target host, checks out the release, installs dependencies in place, and restarts the relevant systemd unit. The only differences are the build command (uv sync for Python, npm
for the Console) and which unit gets restarted.
API / App host
The control-plane host. The Backend API serves admin and runtime-config APIs on :8080; the Console is a static build served by nginx; MongoDB and Redis back the Backend. Small deployments co-locate all four.What runs
systemd unit (Backend)
Jenkins deploy step
Fleet host(s)
The runtime host. Each fleet host runs N single-worker uvicorn processes, one per call slot, each bound to its own Unix socket. nginx fans incoming calls across the sockets withleast_conn and max_conns=1, so each worker handles exactly one call at
a time. Recordings are uploaded to a MinIO instance local to the host.
The worker model
This is the part that does not look like an ordinary web service. Instead of one uvicorn process with many workers, the fleet uses one systemd instance per worker, each pinned to its own socket. That makes a single call the unit of failure and lets you restart one worker without touching the others.| Property | Value |
|---|---|
| Process per worker | 1 uvicorn --workers 1 |
| Socket per worker | /tmp/voxcore_<i>.sock |
| Calls per worker | 1 (MAX_CONCURRENT_CALLS=1) |
| Worker count | N per host (commonly 16 on a 4-CPU / 8 GB host) |
| Host capacity | = number of enabled voxcore@<i> instances |
Templated systemd unit
A single template unit,voxcore@.service, is enabled once per worker as
voxcore@1, voxcore@2, … voxcore@N. The instance number %i selects the socket.
nginx in front of the sockets
nginx terminates public traffic and balances across the worker sockets. Two details are non-negotiable, both shipped in the repo template atinfra/nginx/voxcore-fleet.conf.template:
map $http_upgradefor theConnectionheader. A hardcodedConnection "upgrade"works for WebSocket calls but breaks the HTTP POST routes (dialout returns 422).- 429-retry to the next upstream for the short POST routes (
/attach,/livekit/dialout,/livekit/widget). These return before the call pipeline finishes, soleast_conncan route a later request to a worker that is logically busy; retrying a 429 lets nginx try the next free socket.
Jenkins deploy step
Restart all workers
The glob restarts every worker on the host. For a true zero-downtime roll you can
restart workers one at a time while the rest keep serving.
Scaling the fleet
More workers on a host
Add a socket line to the nginx upstream,
systemctl enable --now voxcore@<i> for the
new instances, then nginx -t && systemctl reload nginx.More fleet hosts
Deploy the fleet to a new host, then add its URL to the Backend’s fleet list (and to
HAProxy if used). No code change on existing hosts — fleet selection picks it up.
SIP / LiveKit host
The telephony host. It runs the LiveKit media server and thelivekit-sip bridge as a
docker-compose stack (the only Docker on the platform), and it hosts the single
Dialler instance.
LiveKit
LiveKit andlivekit-sip are brought up with the docker-compose stack from the repo
(infra/livekit/setup.sh). It holds the SIP trunks per carrier, needs use_external_ip
on cloud hosts, and needs a webhook configured for inbound SIP dispatch. LiveKit is the
one component that is not redeployed by the app pipeline — it is brought up once and
left running.
Dialler
The Dialler is a background asyncio worker (with a small health server), not a web app. It runs from/opt/voxdialler and ticks every LOOP_INTERVAL_SECONDS (2.0): query
running campaigns → predictive pacing → fair-share → lease calls atomically in MongoDB →
place SIP calls via LiveKit → on answer, attach to a free fleet worker through POST /attach.
Jenkins deploy step
WSS ingress host (optional)
When more than one fleet host serves a single public domain, an HAProxy ingress fronts them. Carrier and CRM WSS traffic hits HAProxy, which spreads it across the fleet hosts; each fleet host still runs its own nginx and worker sockets unchanged. The templates live in the fleet repo underinfra/haproxy/ (render.sh, add-fleet.sh,
haproxy.cfg.template). Adding a fleet to the ingress is config-only — render the config
with the new backend and reload HAProxy; no fleet-host code changes.
A release, end to end
Rollback uses the same job in reverse: redeploy the previous release (check out the
prior tag →
uv sync / rebuild → restart the unit). Keep the last-known-good tag handy.
The full set of restart, log, and recovery commands lives in the
runbook.Where to go next
The pipeline
The Bitbucket → Jenkins flow: branch strategy, build, lint, tests, and the gate to deploy.
Operations runbook
Restart commands, health checks, logs, scaling, and rollback for day-to-day ops.
Configuration & secrets
Every
.env var per service and where each one belongs.Repository map
The four repositories and the contracts between them.