Skip to main content
How to run the API, the runtime, the ARQ worker, and the campaign orchestrator as host processes against a containerised database. This is the setup you want when you are editing Python and need a fast feedback loop.
If you only want to use VoicEra, run the whole stack in Docker instead — see Install and run. This page is for changing the code.

What you need

Clone the repository and copy the environment template:
Then generate the three secrets that have no usable default:
Put the first into SECRET_KEY and again into INTERNAL_API_KEY, and the Fernet key into PROVIDER_AUTH_ENCRYPTION_KEY. See Environment variables for what each one does.
There is exactly one .env, at the repository root, plus a separate model-server/.env for the optional model stack. There are no per-app env files. apps/api/app/config.py resolves the root .env four directories up from itself, so running uvicorn from inside apps/api still picks it up.

Repository layout in brief

The full map, including the files that are intentionally empty, is in Repository layout.

The PYTHONPATH and the apps namespace

apps/ is a Python namespace package. apps/runtime imports apps.providers and apps.telephony directly:
For those imports to resolve, the repository root must be on PYTHONPATH. Docker arranges this two different ways, which is why the two services look different when you run them by hand: What resolves what, with the repository root on the path: Running from source, export the repository root once per shell:
There is no pip install -e .pyproject.toml is an empty placeholder, so the project is not pip-installable. Install dependencies with an explicit pip install -r apps/<app>/requirements.txt, and set PYTHONPATH yourself.The Makefile does cover the Docker stack — make application-up, make application-down, make application-logs, and the model-server-* equivalents; run make help for the list. It has no targets for running services from source, which is what the rest of this page describes.

Database-only Compose

Start just the storage layer and leave the application processes to your shell:
FerretDB publishes 27018 on the host and listens on 27017 inside the container, so your .env must point at the host port:
.env.example already ships those values — it is docker-compose.yaml that overrides them to mongodb:27017 for the containers. If you are working on campaigns, the knowledge base, or call artifacts, add the services those need:
Redis backs the ARQ queue, the campaign event bus, and concurrency slots. MinIO holds recordings, transcripts, campaign CSVs, and knowledge-base PDFs. minio-init creates the bucket named by MINIO_BUCKET and then exits.

Running the API

This matches apps/api/Dockerfile’s CMD and the compose command: on the api service, which adds --reload. Confirm it came up:
The interactive OpenAPI console at http://localhost:8000/docs is the always-current route list.
The API imports apps.providers and apps.telephony too, and finds them through the apps/ directory in the repository root when you run from apps/api with the root on PYTHONPATH. If you see ModuleNotFoundError: No module named 'apps', that export is missing.

Running the runtime

The runtime is a separate virtualenv — its requirements pull in Pipecat and its vendor extras, which the API does not need.
Run this from the repository root, not from apps/runtime — the module path is apps.runtime.app. The uvicorn line matches apps/runtime/Dockerfile’s CMD; the compose runtime service declares no command: and so inherits it. apps/runtime/app.py also has a main() entry point, reachable as a module:
That path reads RUNTIME_HOST (default 0.0.0.0) and RUNTIME_PORT (default 7860) and starts uvicorn with reload=False. Use the explicit uvicorn command while developing; use python -m apps.runtime.app when you want the environment variables to pick the bind address. Confirm it came up:
pipecat-ai[deepgram,cartesia,openai,silero,websocket]==1.8.1 is a large install and pulls model weights for Silero VAD on first use. Expect the first pip install and the first call to be slow.

Running the worker and orchestrator

Both run the apps/api package, so reuse the API virtualenv and run them from apps/api. The ARQ worker executes campaign batches and CSV source syncs off the request path:
The campaign orchestrator listens on Redis pub/sub, schedules the next batch, and detects campaign completion:
Both commands are copied from the arq-worker and campaign-orchestrator command: lines in docker-compose.yaml. Neither service listens on a port.
ENABLE_CAMPAIGN_ORCHESTRATOR (default True) lets API startup spawn the orchestrator in-process. Docker runs it as a separate container instead. If you are running the API from source and also start the orchestrator by hand, set ENABLE_CAMPAIGN_ORCHESTRATOR=False so you do not run two.

Hot reload

uvicorn --reload watches the working directory. Because the API and the runtime resolve apps/ differently, reload covers different trees: Editing a provider or telephony vendor while the API is running therefore needs a manual restart. The same is true in Docker: ./apps:/app/apps:ro is mounted read-only into the API container, but uvicorn’s reloader is rooted at /app, whose contents come from ./apps/api.

Linting

The only lint configuration in the repository is model-server/ruff.toml, and it applies to the model-server/ tree:
It sets line-length = 100, target-version = "py312", and selects E, F, W, I, B, UP, SIM, C4, RET, ARG. Vendored model folders are excluded path by path — the file explains why, and the exclusions are deliberately not blanket stt/** / tts/** globs, so files you add inside a vendored folder stay linted. There is no ruff, black, or isort configuration covering apps/. Match the surrounding style: from __future__ import annotations at the top of every module, PEP 8, and type hints on public functions.
There is no CI. Nothing runs lint or tests on a push. Run the test suites and ruff check . yourself before opening a pull request.