GUIDES
Self-hosting Agentifys
What a self-hosted Agentifys deployment involves, what it depends on, and how to get one set up.
The shape of an install
A deployment is two containers on one host, plus a managed Postgres. There is no database container. The backend is not exposed directly: it listens on port 8001 on the internal network, and Caddy is the only thing bound to the public interface.
| Field | Type | Description |
|---|---|---|
web | container | Caddy on :80 and :443. Terminates TLS, serves the built console and the pre-rendered public site, and reverse-proxies /v1, /api, /admin, /static, /ws, /oauth and /health to the backend. |
backend | container | FastAPI on :8001, internal only. The agent runtime, the public /v1 API, the widget script at /static/widget.js, and the embedding and reranking models. |
Postgres + pgvector | external | Managed Supabase. Holds projects, sessions, logs, encrypted model keys and vault credentials, and the embedding vectors for RAG. It also provides the auth layer. |
Retrieval runs entirely inside the backend container. Embeddings come from BAAI/bge-m3 and reranking from a local cross-encoder, both loaded into memory at startup, so no document text is sent to an embedding API. That is also what makes the backend heavy.
| Field | Type | Description |
|---|---|---|
Memoryrequired | 6 GB | The compose file sets a 6 GB limit and a 4 GB reservation. The models plus torch are roughly 2.3 GB resident; below this the backend is killed under load. |
Domain + DNSrequired | required | A real domain with an A/AAAA record pointing at the host before the first start. Caddy issues a Let’s Encrypt certificate on boot and needs inbound ports 80 and 443 open. |
CPU architecture | match | Build on the same architecture as the host. An amd64 image on an arm64 host runs under emulation and is unusably slow. |
First build | ~3 GB | The model weights are baked into the image, so the first build is slow and the image is large. |
The database is not optional
Agentifys is a Postgres application. DATABASE_URL must point at a Postgres database with the vector extension available. On first boot the backend runs CREATE EXTENSION IF NOT EXISTS vector and then its own migrations, so you do not apply a schema by hand.
| Field | Type | Description |
|---|---|---|
Rolerequired | postgres | The connection must use the postgres superuser role. It needs CREATE EXTENSION vector, and read access to auth.users so team invites can bind to a verified account. |
Endpointrequired | session pooler | On Supabase, use the session pooler string (IPv4, port 5432). Not the direct endpoint, which is IPv6-only without a paid add-on, and not the transaction pooler on 6543. |
Extensionrequired | vector | pgvector. Enable it explicitly in the provider console as well, rather than relying on first-boot creation. |
DATABASE_URL is unset the backend does not refuse to start — it falls back to a local SQLite and ChromaDB development mode that is single-tenant and has no auth. In that mode the multi-tenant paths return nothing, including the one that loads a project's provider keys, so every chat request answers PROJECT_NO_API_KEY. It looks like it booted. It is not a deployment.Authentication
Console logins are Supabase Auth. The backend verifies HS256 tokens only, against the project's legacy JWT secret. If the Supabase project's active signing key is asymmetric, every admin login returns 401 — the fix is to use the legacy HS256 secret.
The server refuses to boot on a missing or placeholder JWT secret. If SUPABASE_JWT_SECRET is empty or still set to the local-dev placeholder super-secret-jwt-token-with-at-least-32-characters-long, the process prints a fatal message and exits, because anyone could otherwise forge a token for any account. The one exception is when SUPABASE_URL points at 127.0.0.1 or localhost, where it warns and continues for local development.
Environment
Seven values must be set before the first start. Five of them are read by the backend at runtime; the two VITE_ values are build arguments baked into the console bundle, so changing either means rebuilding the web image.
| Field | Type | Description |
|---|---|---|
SITE_ADDRESSrequired | hostname | The public domain Caddy serves and requests a certificate for, e.g. agent.yourdomain.com. Comma-separate to serve several names from one deployment — Caddy issues a certificate for each. If you do, set PUBLIC_ALIASES too. |
DATABASE_URLrequired | string | Postgres connection string, postgres role, session pooler, against a database with pgvector available. |
SUPABASE_URLrequired | url | The Supabase project URL. A localhost value puts the server into permissive dev behaviour, so in production this must be the real URL. |
SUPABASE_JWT_SECRETrequired | string | The legacy HS256 JWT secret. Missing or placeholder values stop the process at startup. |
ENCRYPTION_KEYrequired | string | The Fernet key that encrypts stored model keys and vault credentials. See below — it has a silent fallback, and it cannot be rotated. |
VITE_SUPABASE_URLrequired | build arg | Same Supabase URL, compiled into the console bundle. |
VITE_SUPABASE_ANON_KEYrequired | build arg | The Supabase anon public key, compiled into the console bundle. |
Everything else is optional and has a working default.
| Field | Type | Description |
|---|---|---|
PUBLIC_BASE_URL | url | Public HTTPS base URL of this deployment, no trailing slash. Fixes the MCP OAuth redirect at <PUBLIC_BASE_URL>/oauth/mcp/callback. Unset means OAuth-protected MCP servers cannot be connected. |
PUBLIC_ALIASES | hostnames | Every OTHER hostname this deployment answers on, comma-separated, when SITE_ADDRESS lists more than one. PUBLIC_BASE_URL names the canonical host and holds one value, so without this the other names are not recognised as this server — and the guard that stops a project registering our own /mcp as its MCP server stops covering them. Renaming a deployment means moving the old hostname in here, not deleting it. |
INDEXING_ENABLED | boolean | Default true. Set to false to pause document upload and indexing — the heavy embedding path — while search over already-indexed documents keeps working. |
EERRAA_EXAMPLE_PROJECT | boolean | Default true. Creates one example project for each new organisation. |
EERRAA_SEED_DEMOS | boolean | Default off. Seeds illustrative demo projects on boot. They carry fake vertical data and no organisation, so leave this off in production. |
EERRAA_ALLOW_LOCALHOST_WEBHOOKS | boolean | Default off. Off blocks webhook tools from calling loopback and private addresses. Turning it on removes that guard, so keep it off outside local development. |
SMTP_HOST | string | Enables the one outbound email Agentifys sends, a webhook-failure notice. With SMTP_HOST unset, no email leaves the server. Project alerts are in-app regardless. |
SMTP_PORT | integer | Default 587. STARTTLS is used when the port is exactly 587. |
SMTP_USER | string | Login and From address. Also the fallback recipient. |
SMTP_PASSWORD | string | SMTP password or app password. |
ALERT_EMAIL_TO | string | Recipient for the webhook-failure notice. Falls back to SMTP_USER. |
ANTHROPIC_API_KEY or OPENAI_API_KEY on the server. The backend deletes both from its own environment at startup so no code path can fall back to a shared platform key. Provider keys belong to a project and are added in the console — see the model keys guide.ENCRYPTION_KEY
ENCRYPTION_KEY is the Fernet key that encrypts every stored provider key and every per-user vault credential. Generate it once, before the first start:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
It must be set explicitly, because the fallback chain is silent. The value is resolved like this:
| Field | Type | Description |
|---|---|---|
Exactly 44 characters | used as-is | The value is treated as a Fernet key directly. This is what the generator above produces, and it is the only intended case. |
Any other length | coerced | The first 32 bytes are taken, right-padded with the character 0 to 32 bytes, and base64-encoded. A longer passphrase silently contributes only its first 32 bytes. |
Unset | derived | The key becomes SHA-256 of SUPABASE_JWT_SECRET — so your secrets at rest are tied to your JWT secret, and rotating that JWT secret makes every stored key undecryptable. |
Unset, with no JWT secret | constant | The key is derived from the literal string dev-fallback-secret, which is in the source. Encryption at rest is then effectively decorative. Production cannot reach this state, because a missing JWT secret already stops the server. |
ENCRYPTION_KEY after go-live does not migrate anything — it makes every stored provider key and vault credential permanently undecryptable, and the only recovery is to re-enter all of them by hand. Treat it as a root secret: generate it once, back it up outside the host, and never change it.What self-hosting does and does not include
Running Agentifys on your own host means the agent runtime, your documents, your embeddings, your logs, and your encrypted credentials sit on infrastructure you control, and retrieval never leaves it. Model calls still go to whichever provider you gave a key to, billed on your account, so you keep that relationship too.
The deployment described above is not fully air-gapped. It depends on managed Supabase for Postgres and for the auth layer, which means your data lives in your Supabase project rather than on your host. If your requirement is that nothing leaves your own network, that is a different build and a conversation to have up front rather than after a deploy.
A production rollout usually needs things around it as well: SSO, a DPA, an SLA, a security review, and named support. Those are agreements, not environment variables.