Skip to content

Deploy

Felix runs as containers you operate — CPython behind your own ingress, with no serverless or edge runtime anywhere in the harness. A CDN may sit in front for DNS, TLS and WAF only.

Piece Local Production
API felix-api on :8080 Same image behind your ingress
Worker Taskiq consumer Same image, felix-worker
Scheduler Taskiq scheduler Same image, felix-scheduler
Postgres + pgvector Compose service Managed Postgres (Neon, RDS, Cloud SQL, …)
Valkey / Redis Compose Memorystore / ElastiCache / self-hosted
Object store fs (default) s3 / gcs (+ image extras)

Compose

Terminal window
cp .env.example .env # set POSTGRES_PASSWORD
make up
make migrate

Lean hosts: make up-lite. MinIO profile: make up-full with FELIX_DOCKER_EXTRAS=aws.

Sizing

Each worker process carries its own connection pool, so raise the two together or the second one becomes the ceiling.

Setting Default Notes
FELIX_WORKERS 1 Granian/uvicorn worker processes
FELIX_DB_POOL_SIZE 10 Per worker
FELIX_DB_MAX_OVERFLOW 20 Per worker, on top of the pool
FELIX_DB_POOL_TIMEOUT_SECONDS 30 How long a request queues before failing
FELIX_DB_POOL_PRE_PING true Set false against a direct Postgres
FELIX_DB_PREPARED_STATEMENTS true Set false behind a pooler that does not track them — see below

The effective ceiling is WORKERS × (POOL_SIZE + MAX_OVERFLOW) connections. Past it, requests queue for FELIX_DB_POOL_TIMEOUT_SECONDS and then fail — so check the ceiling against your database’s own max_connections before raising either number.

FELIX_DB_POOL_PRE_PING costs a round trip on every checkout to discover a connection a pooler closed server-side. That earns its cost behind PgBouncer, RDS Proxy, or Cloud SQL and wastes it against a Postgres you connect to directly.

Behind a connection pooler

Four workers on the defaults is 120 connections against a stock Postgres max_connections of 100 — and raising max_connections trades one wall for another, since every backend costs memory. A transaction-mode pooler multiplexes many cheap client connections onto few server ones instead.

Terminal window
make up-pooled # adds PgBouncer in transaction mode in front of Postgres

Felix’s own pool then becomes a pool of client connections to PgBouncer, which are cheap. PGBOUNCER_POOL_SIZE is the real server-side limit — raise the former freely, size the latter against your database.

Run migrations against Postgres directly rather than through the pooler. Nothing breaks under transaction pooling; a migration is a one-off admin action with no reason to go through a multiplexer sized for request traffic.

Prepared statements

Either the pooler tracks prepared statements or Felix stops making them — never neither:

Pooler Setting
PgBouncer ≥ 1.21 with max_prepared_statements > 0 leave FELIX_DB_PREPARED_STATEMENTS=true
PgBouncer with max_prepared_statements = 0 set FELIX_DB_PREPARED_STATEMENTS=false
RDS Proxy set FELIX_DB_PREPARED_STATEMENTS=false
Direct Postgres, or session-mode pooling leave it true

make up-pooled sets max_prepared_statements non-zero, so the default is correct there.

RDS Proxy is the case that forces the decision rather than merely allowing it: it pins the session when it sees a prepared statement, holding a server connection for the life of the client one — which defeats the multiplexing you deployed it for. There the setting costs you nothing and buys back the pooling.

Helm

Chart: deploy/helm/felix in felix-run/felix.

Terminal window
helm upgrade --install felix ./deploy/helm/felix \
--set secrets.databaseUrl="$FELIX_DATABASE_URL" \
--set secrets.redisUrl="$FELIX_REDIS_URL" \
--set secrets.consumerSharedSecret="$FELIX_CONSUMER_SHARED_SECRET" \
--set secrets.jwksPublic="$FELIX_JWKS_PUBLIC" \
--set secrets.jwksPrivate="$FELIX_JWKS_PRIVATE"

A pre-install/pre-upgrade Job runs felix migrate head (migrate.enabled=true). The chart runs api, worker, and scheduler. For FELIX_OBJECT_STORE=fs, enable persistence.enabled=true.

Cloud notes: deploy/aws/README.md, deploy/gcp/README.md.

Auth (production)

Mode Notes
jwt Preferred — set FELIX_JWKS_PUBLIC / verifiers
api_key FELIX_AUTH_API_KEYS map
none Loopback only — see below

none cannot be exposed

FELIX_AUTH_MODE=none is refused whenever FELIX_HOST is reachable off-host — in every environment. FELIX_ALLOW_INSECURE only relaxes the non-development environment check; it does not permit a public unauthenticated bind. FELIX_HOST defaults to 127.0.0.1, containers set 0.0.0.0 explicitly and pair it with a real auth mode, and Compose defaults to api_key with a key generated by make up.

/internal/* needs FELIX_CONSUMER_SHARED_SECRET when auth is not open.

Cron schedules

Scheduled jobs (PUT /jobs/{name}) use standard five-field cron, evaluated in UTC:

┌─ minute (0-59)
│ ┌─ hour (0-23)
│ │ ┌─ day of month (1-31)
│ │ │ ┌─ month (1-12)
│ │ │ │ ┌─ day of week (0-6, Sunday = 0)
│ │ │ │ │
* * * * *
Expression Meaning
*/10 * * * * Every ten minutes
0 3 * * * Daily at 03:00
0 9 * * 1 Mondays at 09:00

The worker’s run_scheduled_jobs cron sweeps due rows every minute, but the sweep is enqueued by felix-scheduler. Run all three containers — API, worker and scheduler — or no job, retention sweep, memory consolidation or fiber resume ever fires.

Model routes

FELIX_DEFAULT_MODEL_ID (default claude-sonnet) picks the model a manifest gets when it does not name one. FELIX_MODEL_ROUTES is a JSON object that overrides or extends the logical-id table, so manifests stay portable across deployments:

Terminal window
FELIX_DEFAULT_MODEL_ID=claude-sonnet
FELIX_MODEL_ROUTES='{
"claude-sonnet": {"provider": "anthropic", "model": "claude-sonnet-5"},
"house-fast": {"provider": "ollama", "model": "llama3.2"}
}'

Providers are anthropic, openai, ollama, workers_ai (Cloudflare Workers AI), groq, together, deepseek, cerebras, fireworks, openrouter, xai, mistral and google, plus any registered by a plugin. Point FELIX_OLLAMA_BASE_URL or FELIX_LITELLM_BASE_URL at a local gateway to keep traffic in your own network. Everything past the first three is configured through FELIX_MODEL_PROVIDER_OPTIONS:

Terminal window
FELIX_MODEL_PROVIDER_OPTIONS='{"workers_ai":{"api_key":"...","account_id":"...","gateway_id":"default"}}'
FELIX_MODEL_ROUTES='{"cf-fast":{"provider":"workers_ai","model":"@cf/meta/llama-3.3-70b-instruct-fp8-fast"}}'

Provider names in FELIX_MODEL_ROUTES are resolved against the registry at startup, so a typo fails immediately rather than on the first request that happens to take that route. A manifest referring to house-fast works everywhere the route is defined. See Model client.

Chat UI / docs Workers

Frontends live in felix-run/web. Deploy chat-ui with vars.FELIX_ORIGIN pointing at your public Felix API; docs is a static Workers site.

Where to next