CI/CD pipeline
How a change travels from a developer's git push to running on the servers — the Bitbucket → Jenkins pipeline, the branch strategy, per-service lint/test/build stages, and the Jenkins-to-host deploy model.
Code does not move by hand. Every change to any of the four services flows through
version control and a build server before it lands on a host. The path is always the
same shape: push to Bitbucket → webhook fires Jenkins → lint and test → on green
main, Jenkins deploys to each target host → restart systemd → health check.
There is no rsync, no manual git pull on a box, and no container registry for the
app services. Jenkins itself is the deploy agent: on a passing build it checks out the
release on each host, runs the per-service install/build, restarts the systemd
service, and verifies the health endpoint.
The pipeline at a glance
The split in that diagram is the whole model. CI runs on every branch and pull
request — it only proves the code is good. CD runs only when main is green —
that is the single event that puts code on a server. A red build on a feature branch
never reaches a host.
Branch strategy
The branch model is deliberately small.
Branch off main
Every change starts on a short-lived feature branch cut from main
(feature/..., fix/...). You push it to Bitbucket and open a pull request
back into main.
CI gates the PR
The push fires the Jenkins pipeline. Lint, tests, and the build must pass before the PR can be merged. CI on a branch never deploys anything.
Merge to main
Once reviewed and green, the PR merges into main. The merge commit is the
deployable unit.
Deploy from main
A green build on main is what triggers the deploy stage. You only ever deploy
from main — feature branches are build-and-test only.
Tip
Tag the commit you deploy (for example vox-backend-2026.06.22-1). The rollback
procedure is simply to re-run the same Jenkins deploy job pointed at the previous
tag — keep a last-known-good tag handy. See the operations runbook.
Which repo deploys where
Each repository targets one host role. The repo names below are the real Bitbucket
repositories (git@bitbucket.org:oriserve1/<name>.git); everywhere else in these docs
we use the friendly service names.
| Repository | Service (prose) | Target host role | What the deploy step does |
|---|---|---|---|
vox-backend | Backend API | API / App host | uv sync → restart the voxbridge uvicorn service on :8080 |
vox-frontend | Console | API / App host (nginx static root) | npm ci && npm run build → publish dist/, nginx serves it |
vox-agents | Voice fleet | Fleet host(s) | uv sync → restart all voxcore@* worker instances |
vox-dialler | Dialler | SIP / LiveKit host (single instance) | uv sync → restart the voxdialler service |
For the full host-role layout — which processes co-locate, the nginx and HAProxy fronting, MinIO, LiveKit, MongoDB and Redis placement — see the deployment topology page.
The Dialler is the one repo with a placement rule baked into deploy targeting: there
must be exactly one Dialler instance per database. It runs on the SIP/LiveKit
host. Two diallers against the same MongoDB will over-dial, because each one paces as
if it were alone. Never add a second deploy target for vox-dialler.
Per-service pipeline stages
The CI stages differ only in the tools each service uses. The shape is identical: install, lint, test, build.
All three Python services use uv (never pip). requires-python >= 3.12.
# 1. install pinned dependencies into the project venv
uv sync
# 2. lint
uv run ruff check .
# 3. test
uv run pytest -vuv sync is also the install step on the host at deploy time — the same command
that builds the CI venv reconstructs the runtime venv on the server, so what you
test is what you run.
The Fleet (vox-agents) pins Pipecat to a specific version in pyproject.toml.
uv sync respects the lockfile, so CI and the host always resolve the identical
dependency tree — never bump it loosely.
The Console is a React 19 + Vite 8 single-page app built with npm.
# 1. clean, reproducible install from the lockfile
npm ci
# 2. lint
npm run lint # eslint .
# 3. type-check + build (npm run build === tsc -b && vite build)
npm run build
# 4. test
npm testnpm run build runs the TypeScript project build (tsc -b) before vite build,
so a type error fails the pipeline at build time. The output is a static dist/
bundle — there is no Node server in production; nginx serves the static files.
Console branding is build-time: the Ori brand .env (the VITE_* variables,
including VITE_API_URL pointing at the Backend) is committed in the repo and copied
to the root .env before vite build. For Ori's single-brand repo the deploy build
is just npm ci && npm run build. Changing a brand value means a rebuild, not a
config reload.
Illustrative bitbucket-pipelines.yml
If you wire CI on the Bitbucket side, a bitbucket-pipelines.yml like the one below
covers a Python service: lint and test on every branch, and a manual deploy step
gated to main that hands off to Jenkins.
This is a template to adapt, not a file that is committed to the repos. The Ori pipeline runs on Jenkins; the snippet here exists only to show the stage shape if you choose to drive it from Bitbucket Pipelines instead.
# bitbucket-pipelines.yml — ILLUSTRATIVE (Python service: vox-backend / vox-agents / vox-dialler)
image: python:3.12-slim
definitions:
steps:
- step: &lint-and-test
name: Lint & test
caches:
- pip
script:
- pip install uv
- uv sync
- uv run ruff check .
- uv run pytest -v
pipelines:
# every branch & PR — CI only, never deploys
branches:
'**':
- step: *lint-and-test
# main — CI, then a manual gate that triggers the Jenkins deploy job
pipelines:
main:
- step: *lint-and-test
- step:
name: Trigger deploy (main only)
trigger: manual
script:
- echo "Green on main — notifying Jenkins to deploy from main"
- curl -fsS -X POST "$JENKINS_DEPLOY_HOOK"For the Console, swap the image and the three commands for the npm flow
(npm ci, npm run lint, npm run build, npm test).
Illustrative declarative Jenkinsfile
Jenkins is where the real deploy lives. A declarative Jenkinsfile runs the CI stages,
then — only on main — checks out the release on each target host, installs/builds,
restarts systemd, and runs the post-deploy health check.
Also a template to adapt — no Jenkinsfile is committed to the service repos. It
is shown here so the deploy stages and the Jenkins-to-host model are concrete. Adjust
the agent labels, host targets, and credentials to your Jenkins setup.
// Jenkinsfile — ILLUSTRATIVE declarative pipeline for a Python service (e.g. vox-backend)
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
}
stages {
stage('Install & lint') {
steps {
sh 'uv sync'
sh 'uv run ruff check .'
}
}
stage('Test') {
steps {
sh 'uv run pytest -v'
}
}
// CD only runs on main — feature branches stop after Test
stage('Deploy from main') {
when { branch 'main' }
steps {
// On the target host: check out the release, rebuild the venv, restart, verify.
sshagent(['deploy-key']) {
sh '''
ssh deploy@api-host '
cd /opt/voxbridge &&
git fetch --all --tags &&
git checkout main &&
git reset --hard origin/main &&
uv sync &&
sudo systemctl restart voxbridge
'
'''
}
}
}
stage('Post-deploy health check') {
when { branch 'main' }
steps {
// App boots only if required settings are present; a 200 means it came up clean.
sh 'curl -fsS http://api-host:8080/health'
}
}
}
post {
failure {
echo 'Build or deploy failed — main was NOT shipped. Re-run against last-known-good tag to roll back.'
}
}
}The per-service deploy step differs only in the install/build and restart commands:
# API / App host
cd /opt/voxbridge
git checkout main && git reset --hard origin/main
uv sync
systemctl restart voxbridge
curl -fsS http://localhost:8080/health# Fleet host — restart every worker instance behind nginx
cd /opt/voxcore
git checkout main && git reset --hard origin/main
uv sync
systemctl restart 'voxcore@*'
curl -fsS http://localhost/health/fleet# SIP / LiveKit host — single instance only
cd /opt/voxdialler
git checkout main && git reset --hard origin/main
uv sync
systemctl restart voxdialler
curl -fsS http://127.0.0.1:8090/health# API / App host — build static bundle, nginx serves dist/
cd /opt/voxui
git checkout main && git reset --hard origin/main
npm ci
npm run build # tsc -b && vite build
# publish dist/ to the nginx root, then reload nginxThe deploy step, in detail
On a green main, the deploy stage is the same five moves for every service.
Check out the release on the target host
Jenkins connects to each target host for that repo and checks out the main
commit (or the chosen tag). No artifact is shipped for the Python services — the
host builds its own venv from the same lockfile CI used.
Install or build
Python services run uv sync to reconstruct the runtime venv. The Console runs
npm ci && npm run build and publishes the static dist/ to the nginx root.
Restart systemd
The service unit is restarted: voxbridge for the Backend, the templated
voxcore@* instances for the Fleet (each is one worker = one call slot),
voxdialler for the Dialler. Restarting one fleet worker (voxcore@2) is
zero-downtime — the others keep serving.
Post-deploy health check
Jenkins curls the service health endpoint and fails the build if it does not come back clean — so a deploy that boots into a bad state is visible immediately, not on the next call.
Done — or roll back
Green health check ends the deploy. If anything failed, re-run the same Jenkins job pinned to the previous tag to roll back. Detailed restart, scaling, and rollback commands live in the runbook.
Health endpoints the deploy checks
| Service | Endpoint | What a healthy response confirms |
|---|---|---|
| Backend API | GET :8080/health | App booted with all required settings present |
| Voice fleet | GET /health (per worker) · GET /health/fleet (aggregate) | Workers up; aggregate shows worker_calls/worker_max and fleet_available |
| Dialler | GET :8090/health · GET :8090/metrics | Tick loop alive; metrics being emitted |
No Docker images are built for the app services. The Backend, Console, Fleet, and
Dialler all deploy as native processes managed by systemd (uvicorn services for the
Python apps, a static nginx bundle for the Console). The only containers in the stack
are LiveKit and livekit-sip, which stay on docker-compose on the SIP/LiveKit
host — they are infrastructure, not part of the app CI/CD path.
Where to go next
Deployment topology
The host roles in full — API/App, fleet, SIP/LiveKit — and what runs where.
Configuration & secrets
The .env files each service needs, and how secrets reach the hosts.
Operations runbook
Restart, scale, roll back, and read logs once code is live.
Repository map
The four repositories and the contracts between them.
Configuration reference
Every environment variable for the four Ori services — Backend, Voice fleet, Dialler, and Console — grouped by service, with meaning, default, and which ones are required to boot.
Deployment
How a release lands on the servers — the host roles, the systemd units that run each service, and how Jenkins checks out, builds, and restarts them.