Skip to main content
Every variable the VoicEra stack reads, its default, and whether you must set it. Values come from .env.example, docker-compose.yaml, apps/api/app/config.py, apps/runtime/constants.py, and model-server/.env.example.
There are exactly two env files: one .env at the repository root, and a separate model-server/.env. There are no per-service env files. If you are following older documentation that describes five per-service .env files, that layout no longer exists.

The single root .env

Copy the template and edit it in place:
Every service in docker-compose.yaml that needs configuration loads this one file through env_file: - .env. The API also reads it directly outside Docker: apps/api/app/config.py pins env_file to the repository root .env, resolved four directories up from config.py, so running uvicorn from apps/api still picks up the root file. make application-up (which wraps ./scripts/start-application-services.sh) generates SECRET_KEY, INTERNAL_API_KEY, and PROVIDER_AUTH_ENCRYPTION_KEY if they are missing. Prefer it over a bare docker compose up — the compose file’s own header says a fresh checkout without .env will fail or come up misconfigured. The model server is a separate stack with its own compose file and its own model-server/.env. See Model server below.

Infrastructure (Docker Compose)

These only affect how Compose wires the stack. Nothing in application code reads them.

API

Read by apps/api/app/config.py (a pydantic BaseSettings), plus the ARQ worker and the campaign orchestrator, which run the same image. Rotating PROVIDER_AUTH_ENCRYPTION_KEY makes every stored ProviderAuth blob undecryptable. Existing provider credentials must be re-entered after a rotation. Generate the three secrets by hand if you are not using the start script:

Runtime

apps/runtime/constants.py reads these with os.getenv at call time, so a value takes effect on the next call rather than at import. RUNTIME_HOST and RUNTIME_PORT are absent from .env.example; they only matter when you start the runtime through its main() entry point rather than a uvicorn command line.

Dashboard

NEXT_PUBLIC_* variables are baked into the JavaScript bundle at build time. They are not secrets — anyone can read them in the shipped bundle. Both REST and the browser test-call WebSocket are same-origin from the browser: /api/v1/... and /agent/... hit the dashboard host, and next.config.ts rewrites them to API_PROXY_TARGET and RUNTIME_PROXY_TARGET server-side. Neither FastAPI nor the runtime need a public hostname of their own for the dashboard. FRONTEND_URL in the API section is a separate variable: the API uses it to build password-reset links, and it is read server-side.

Redis and campaigns

DEFAULT_ORG_CONCURRENCY_LIMIT is read twice: as a pydantic setting in apps/api/app/config.py and directly with os.getenv in apps/api/app/constants/campaign.py, where it is clamped to a minimum of 1.

Object storage

MinIO holds call recordings, transcripts, campaign source CSVs, and knowledge-base PDFs. MINIO_ROOT_USER / MINIO_ROOT_PASSWORD configure the MinIO server; MINIO_ACCESS_KEY / MINIO_SECRET_KEY are what the applications authenticate with. .env.example ships them as the same pair, so changing only one half locks the applications out.

Knowledge base

Read only by the API. Chroma is embedded, not a service.
KB_EMBEDDING_API_KEY is described in apps/api/app/config.py as a temporary global key. Every organisation’s documents are embedded through the same account.

Model server

A separate stack with its own compose file and its own model-server/.env. Copy model-server/.env.example to model-server/.env. The variables below are the ones you set to run it; the long tail of CORE_*, MIO_*, and MPS tuning knobs ships commented out at the values upstream benchmarks at, and leaving them commented is the supported configuration. A slot counts as deployed when its *_MODEL is named. That is the same variable that picks the build folder, so Compose and the gateway cannot disagree about what is running.

Compose override precedence

Compose applies env_file first, then the environment: block, and environment: wins. Several variables are therefore different inside the stack than in your .env: Editing one of these in .env changes nothing for the containers. To change them inside the stack, edit docker-compose.yaml. DEBUG is the one variable deliberately not interpolated in docker-compose.yaml — it reaches the API through env_file only. The compose file comments why: host shells frequently export DEBUG=release, which would override a boolean False from .env. apps/api/app/config.py defends against this too, with a validator that treats only 1, true, yes, and on as true and everything else as false, so an exported DEBUG=release degrades to False instead of crashing settings parsing.

Variables with no default

Three variables have no usable default. SECRET_KEY is the only one Compose enforces: VOICE_SERVER_BASE_URL is a fourth: it defaults to empty and only matters once you create a telephony agent, at which point it is required.