Deployment¶
VERA is one container image that runs three processes, differing only by command: the API, the MCP server, and the ingestion worker. PostgreSQL and S3 are authoritative; Neo4j is a rebuildable projection; Valkey is cache and rate limiter.
The Image¶
The release workflow publishes the image to GHCR as a public package, so most deployments just pull it:
docker pull ghcr.io/kkloudtarus/vera:latest # or a version tag, e.g. :0.2.0
docker build -t vera:local . # or build locally
Multi-stage, non-root, runtime dependencies only. The base is pinned to
python:3.11-slim-bookworm; the builder patches pip/setuptools/wheel and the runtime applies
OS security updates, so no known-vulnerable package ships (verified by pip-audit).
Commands:
- API (default):
uvicorn vera.entrypoints.api.main:app --host 0.0.0.0 --port 8000 - Worker:
python -m vera.entrypoints.worker.main - MCP:
python -m vera.entrypoints.mcp.main
Docker Compose (single host)¶
This runs migrate, then the API (:8000), worker, and MCP (:8080), alongside the infra
(postgres, neo4j, valkey, minio). The default docker compose up (no profile) starts only
the infrastructure, which is what local development uses.
To use FalkorDB instead of Neo4j in Compose, set VERA_MEMORY__GRAPH_BACKEND=falkordb and
run docker compose --profile app --profile falkordb up --build. Host-run processes use
VERA_FALKOR__HOST=localhost and VERA_FALKOR__PORT=6380; containers use the service name
falkordb and its internal port 6379.
Kubernetes¶
Manifests are in deploy/k8s/:
| File | Contents |
|---|---|
base.yaml |
namespace vera, vera-config ConfigMap, vera-secrets Secret, and the vera-migrate Job (alembic upgrade head) |
api.yaml |
API Deployment + Service, CPU HPA, liveness/readiness probes |
mcp.yaml |
MCP Deployment + Service |
worker.yaml |
worker Deployment, KEDA ScaledObject on ingestion queue depth (CPU HPA fallback) |
calibrate-cronjob.yaml |
nightly rerank calibration (calibrate --apply) |
Apply:
# 1. Put real values in vera-config (non-secret) and vera-secrets (DSN, OpenAI key, JWT
# secret, Neo4j password) in base.yaml, or manage them with your secrets tooling.
kubectl apply -f deploy/k8s/base.yaml # namespace, config, secret, migrate Job
kubectl -n vera wait --for=condition=complete job/vera-migrate
kubectl apply -f deploy/k8s/api.yaml -f deploy/k8s/mcp.yaml -f deploy/k8s/worker.yaml
kubectl apply -f deploy/k8s/calibrate-cronjob.yaml
Config and secrets are injected via envFrom (the ConfigMap and Secret). The worker scales
on pending queue depth with KEDA; delete the ScaledObject and add a CPU HPA if KEDA is not
installed. Per-group serialization is enforced in-process, so worker replicas are safe.
The deploy/k8s/ manifests are the app tier only and assume PostgreSQL, the graph, Valkey,
and the object store are provided externally (managed services or your own).
Helm (full Stack in One namespace)¶
For a single-cluster install of the whole stack, including the datastores, use the chart in
deploy/helm/vera (see its README for the full walkthrough). It installs the API, MCP, and
worker plus PostgreSQL, a graph backend (neo4j by default, falkordb with one flag),
Valkey, and MinIO, with a post-install migrate hook, a MinIO bucket bootstrap, and
health-gated pods. It pulls the published image ghcr.io/kkloudtarus/vera, so no local build
is needed.
The defaults boot with no external credentials (a deterministic embedder and an unauthenticated local MCP principal). For real use, set an embedder key, turn on auth, close self-service signup, and seed an init admin:
helm upgrade vera deploy/helm/vera -n vera \
--set memory.embedder=voyage --set voyage.apiKey=pa-... \
--set rerank.crossEncoderEnabled=true --set rerank.crossEncoderProvider=voyage \
--set environment=prod --set api.authRequired=true --set mcp.jwtSecret=<secret> \
--set api.registrationOpen=false \
--set bootstrap.enabled=true --set bootstrap.adminApiKey=vera_<prefix>.<secret>
With api.registrationOpen=false, POST /identity/register returns 403. The post-install
bootstrap Job (python -m vera.entrypoints.bootstrap_admin) then seeds one init admin from
VERA_BOOTSTRAP__*, idempotently, so a closed deployment still has a first principal; that
admin provisions everyone else through POST /identity/users. For GitOps, keep the admin key
out of git: leave secrets.create=false and put VERA_BOOTSTRAP__ADMIN_API_KEY in the
externally created vera-secrets Secret instead of passing it on the command line.
The command above enables the API-key-to-JWT fallback. For browser OAuth, pair MCP with an external OIDC authorization server that supports authorization code + PKCE and either dynamic client registration or pre-registered coding-tool clients:
helm upgrade vera deploy/helm/vera -n vera \
--set mcp.authAudience=https://mcp-vera.example.net \
--set mcp.oauthIssuer=https://login.example.net \
--set mcp.oauthJwksUrl=https://login.example.net/.well-known/jwks.json
The IdP must issue JWT access tokens for that audience and the memory:read,
memory:propose, memory:feedback, and memory:snapshot scopes. Keep mcp.jwtSecret
during rollout; remove it only after OAuth discovery, browser login, refresh, and an MCP tool
call pass end to end. To link pre-provisioned users by email, its access token must also carry
the trusted email and email_verified: true; otherwise a personal-only principal is created.
With both configured, OAuth is advertised while fallback JWTs remain valid.
flowchart TB
ING[Ingress nginx + TLS] --> SA[vera-api Service] & SM[vera-mcp Service]
SA --> DA[api Deployment]
SM --> DM[mcp Deployment]
DW[worker Deployment: KEDA-scaled]
JM[[migrate Job]] --> JB[[bootstrap Job: seeds init admin]]
CC[[calibrate CronJob]]
subgraph Data[cluster-internal datastores]
PG[(PostgreSQL)]
GR[(Neo4j / FalkorDB)]
VK[(Valkey)]
MO[(MinIO / S3)]
end
DA & DM & DW --> PG & GR & VK & MO
SEC[(vera-secrets)] -. envFrom .-> DA & DM & DW & JB
Two services are meant to be exposed through an ingress: vera-api (:8000) and vera-mcp
(:8080). The chart can render the ingress and cert-manager TLS directly (ingress.enabled,
ingress.apiHost, ingress.mcpHost). The MCP public host must match mcp.authAudience and
the mcp.allowedHosts list. The datastores stay cluster-internal.
Configuration¶
Every setting is an environment variable VERA_<SECTION>__<FIELD> (see
src/vera/config/settings.py). The ones that matter most in production:
| Variable | Purpose |
|---|---|
VERA_DB__DSN |
PostgreSQL DSN (source of truth) |
VERA_MEMORY__PROVIDER |
graphiti to enable the graph |
VERA_MEMORY__GRAPH_BACKEND |
graph backend: neo4j (default) or falkordb (driver shipped in the image since 0.2.0) |
VERA_NEO4J__URI / USER / PASSWORD |
Neo4j graph backend |
VERA_FALKOR__HOST / PORT / PASSWORD |
FalkorDB graph backend (when selected) |
VERA_MEMORY__OPENAI_API_KEY, VERA_MEMORY__EMBEDDER |
LLM extraction and embeddings |
VERA_VOYAGE__API_KEY, VERA_VOYAGE__EMBEDDING_MODEL, VERA_VOYAGE__EMBEDDING_DIM, VERA_VOYAGE__RERANK_MODEL |
Voyage AI embeddings/reranking (optional); dimension changes require group reprocessing |
VERA_RERANK__CROSS_ENCODER_ENABLED, VERA_RERANK__CROSS_ENCODER_PROVIDER |
stage-3 reranker (llm or voyage) |
VERA_OBJECTSTORE__* |
S3-compatible object store |
VERA_RESILIENCE__VALKEY_URL |
shared cache and rate limiter |
VERA_MCP__JWT_SECRET, VERA_MCP__AUTH_ISSUER, VERA_MCP__AUTH_AUDIENCE |
Built-in MCP JWT fallback verification and non-expiring regular-user token issuance |
VERA_MCP__OAUTH_ISSUER, VERA_MCP__OAUTH_JWKS_URL, VERA_MCP__OAUTH_ALGORITHMS |
External OAuth discovery and OIDC access-token verification; use OAUTH_SIGNING_KEY only for static-key testing |
VERA_API__AUTH_REQUIRED, VERA_API__REGISTRATION_OPEN |
require a principal on the API; open or close self-service signup |
VERA_BOOTSTRAP__ENABLED, VERA_BOOTSTRAP__ADMIN_API_KEY, VERA_BOOTSTRAP__ADMIN_EMAIL |
seed the init admin on a closed deployment (the key is a secret) |
VERA_CONNECTORS__SPECS |
scheduled connectors (JSON) |
Keep secrets out of the ConfigMap and out of VERA_CONNECTORS__SPECS; connectors read their
tokens from environment variables named by token_env.
Operational Commands¶
Run these as one-off Jobs (same image) or locally against the same config:
python -m vera.entrypoints.bootstrap_admin # idempotently seed the init admin (VERA_BOOTSTRAP__*)
python -m vera.entrypoints.create_source ... # create a knowledge source
python -m vera.entrypoints.reprocess <group> # rebuild a group's graph from Postgres, then verify
python -m vera.entrypoints.backfill_embeddings <group> # embed canonical entity names
python -m vera.entrypoints.backfill_chunk_embeddings <group> # embed passage/code chunks (dense retrieval)
python -m vera.entrypoints.backfill_fact_embeddings <group> # embed facts (dense retrieval)
python -m vera.entrypoints.calibrate --apply # calibrate rerank weights from feedback
python -m vera.entrypoints.retrieval_eval golden.json # score retrieval quality (CI gate)
python -m vera.entrypoints.dedup_eval pairs.json # measure the dedup threshold and judge
Disaster Recovery¶
PostgreSQL and S3 are backed up; Neo4j is rebuilt from them. See ../docs/dr-runbook.md for the full procedure, including the automated post-rebuild verification and the embedding-model change process.
Health and Observability¶
- Liveness
GET /health/live, readinessGET /health/ready(checks db, graph, object store). The API exposes/metrics; the worker uses:9100/metrics; and MCP uses a scrape-only:9101/metricslistener that is not part of its public Service. All are enabled byVERA_OBSERVABILITY__METRICS_ENABLED=true, and the Kubernetes manifests advertise each endpoint with Prometheus scrape annotations. - Run one MCP ASGI worker per process when metrics are enabled. Scale MCP with pod/container
replicas; multiple workers in one process group cannot share the process-local
:9101listener. - Set
VERA_OBSERVABILITY__OTLP_ENDPOINTto export OpenTelemetry traces from API, worker, and MCP processes.