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.
1

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.
2

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.
3

Merge to main

Once reviewed and green, the PR merges into main. The merge commit is the deployable unit.
4

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.
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.
RepositoryService (prose)Target host roleWhat the deploy step does
vox-backendBackend APIAPI / App hostuv sync → restart the voxbridge uvicorn service on :8080
vox-frontendConsoleAPI / App host (nginx static root)npm ci && npm run build → publish dist/, nginx serves it
vox-agentsVoice fleetFleet host(s)uv sync → restart all voxcore@* worker instances
vox-diallerDiallerSIP / 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 -v
uv 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.

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

The deploy step, in detail

On a green main, the deploy stage is the same five moves for every service.
1

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.
2

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.
3

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.
4

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.
5

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

ServiceEndpointWhat a healthy response confirms
Backend APIGET :8080/healthApp booted with all required settings present
Voice fleetGET /health (per worker) · GET /health/fleet (aggregate)Workers up; aggregate shows worker_calls/worker_max and fleet_available
DiallerGET :8090/health · GET :8090/metricsTick 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.