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
cp .env.example .env # set POSTGRES_PASSWORDmake upmake migrateLean 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.
make up-pooled # adds PgBouncer in transaction mode in front of PostgresFelix’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.
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:
FELIX_DEFAULT_MODEL_ID=claude-sonnetFELIX_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:
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
- Getting Started — local smoke
- REST API — surfaces
- Root README — lean extras / warehouse