Checklist
The go-live pass for a self-hosted Future AGI instance: real secrets, production mode, public URLs, pinned images, isolation, backups, managed stores.
Run through this once before anyone else can reach the instance. The list is the same for every setup:
- Distributed has a production overlay that refuses to start while part of it is missing: see Start Distributed with the production overlay.
- Standalone has no production overlay, so you check each item yourself in
.env. - Helm (Kubernetes) sets the same things through its values: see Install for production.
Apply a .env change with docker compose up -d, which recreates every container whose configuration changed and keeps the data volumes. With the production overlay, use the overlay’s own command, and put each key in the file Which file a key goes in names.
Decide on telemetry
Decide before the first account exists. A self-hosted install sends deployment telemetry to Future AGI by default. Right after the first account is created, which ./bin/install does, it sends a registration with the email addresses of the owner, admin, staff and superuser accounts, then usage counts.
- To opt out from the start, install with
./bin/install --no-telemetry, or with the production overlay, setFUTURE_AGI_TELEMETRY_DISABLED=trueindeploy/.env.productionbefore you start the stack. - Setting
FUTURE_AGI_TELEMETRY_DISABLED=truelater stops the heartbeats, but the registration already sent cannot be taken back.
What is sent, and how to send nothing: Telemetry.
Replace published defaults with real secrets
SECRET_KEY, PG_PASSWORD, MINIO_ROOT_PASSWORD, AGENTCC_INTERNAL_API_KEY, AGENTCC_ADMIN_TOKEN and CH_PASSWORD each need 32 random bytes, none left at a default published in the repository. INTEGRATION_ENCRYPTION_KEY is set once and kept.
- Fresh install:
./bin/installfills every one of them that is empty or aCHANGEME-placeholder with a random value and writes it to.env. It also fillsINTEGRATION_ENCRYPTION_KEY,AGENTCC_WEBHOOK_SECRETand, on Standalone,REDIS_PASSWORD. It never changes a value afterwards. - Installs made before Standalone became the default: the installer never changes existing secrets. It fills only
INTEGRATION_ENCRYPTION_KEYandAGENTCC_WEBHOOK_SECRETwhen they are unset, and warns about the rest. Look in.envfor values that start withlocal-dev-only-, aPG_PASSWORDorMINIO_ROOT_PASSWORDoffutureagi, and secret keys that are empty or commented out, which fall back to those defaults. An emptyCH_PASSWORDmeans ClickHouse’sdefaultuser has no password, and code evals can then read and change every trace. - Without the installer: set them yourself before the first start, together with
INTEGRATION_ENCRYPTION_KEY,REDIS_PASSWORDon Standalone (its default is published, and code evals can reach that Redis), andAGENTCC_WEBHOOK_SECRETon Distributed (empty, the app refuses the gateway’s request logs):
openssl rand -hex 32 # once for each secret
python3 -c "import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" # INTEGRATION_ENCRYPTION_KEY
./bin/install never generates PROPERTY_CATALOG_API_PASSWORD or PROPERTY_CATALOG_CONSUMER_PASSWORD, so outside the production overlay every install runs them on the published defaults oss-observed-reader-local-only and oss-observed-writer-local-only. They are the passwords of the observed-attribute catalog’s two ClickHouse users, which accept logins from any host, and code evals can reach ClickHouse except in Standalone’s built-in sandbox on a Docker host with Linux 6.7 or later and Landlock (see Isolate the code executor). Set both to openssl rand -hex 32 values in .env and run docker compose up -d: the bootstrap sets the two users’ passwords on every start.
What changing each secret later does, and why PG_PASSWORD, CH_PASSWORD and INTEGRATION_ENCRYPTION_KEY must keep their first values, is in Secrets the installer generates.
Turn off local mode
Set ENV_TYPE=production in .env (the production overlay sets it for you). Any value other than local turns off the self-host fallbacks:
DEBUGis off.- Links built from an
APP_URLwithout a scheme usehttps://for any host other thanlocalhostor a loopback address. - An empty
INTEGRATION_ENCRYPTION_KEYmakes connecting an integration fail, instead of using a throwaway key that does not survive a restart. - New uploads go to the bucket
fi-contentinstead offi-content-dev. On an install that already holds files, also setUPLOAD_BUCKET_NAME=fi-content-devin.env, or the app looks for knowledge-base files and other stored files in the new, empty bucket. On a new install, setENV_TYPEbefore anyone stores a file.
Note
ENV_TYPE=production makes the app refuse to start only on Django’s own placeholder SECRET_KEY. The compose files pass their published default, local-dev-only-not-for-production-replace-me, when SECRET_KEY is empty, and the app accepts that one, so production mode does not replace the secrets step above.
Set the public URLs
Point every URL at the HTTPS name people and SDKs reach, then put TLS in front of the UI, the API and object storage as Security & TLS describes. With the production overlay, BASE_URL goes in the root .env and the others in deploy/.env.production: see Which file a key goes in.
| Key | Set it to | What breaks when it is wrong |
|---|---|---|
VITE_HOST_API | The API as browsers reach it, e.g. https://api.example.com | The UI loads but every API call fails, often reported as a CORS error |
BASE_URL | The API’s own public URL | SSO and OAuth callbacks, and the SDK and MCP endpoints shown in the UI |
FRONTEND_URL | The UI’s URL, e.g. https://app.example.com | Links in digest and notification emails, and the setup screen’s SSL/TLS check |
APP_URL | The UI’s URL, with its scheme | Invite, verification and password-reset links |
MINIO_URL | The object storage URL browsers reach, e.g. https://files.example.com | File previews and downloads fail while the rest works |
FI_COLLECTOR_PUBLIC_URL | The trace collector URL SDKs send to | The SDK snippet in the app and on the setup screen points at an address that does not answer |
Lock down hosts and origins
ALLOWED_HOSTS defaults to *. List your API host names instead. On Distributed also add backend,localhost,127.0.0.1: the workers call the API as backend, and ./bin/install checks it on localhost. Standalone adds 127.0.0.1,localhost itself.
CORS_ALLOWED_ORIGINS is empty by default, which allows any origin. Set it to your UI’s origin.
ALLOWED_HOSTS=api.example.com,backend,localhost,127.0.0.1 # Distributed; Standalone: api.example.com
CORS_ALLOWED_ORIGINS=https://app.example.com
With the production overlay, both go in the root .env. No compose file passes them on, so in deploy/.env.production they never reach the API, which then keeps answering every host and origin.
Pin image versions
Pin a release tag (vX.Y.Z), never latest or local. An empty version variable means latest.
- Standalone:
FUTURE_AGI_VERSION, plusSERVING_VERSIONwith themlprofile andCODE_EXECUTOR_VERSIONwith thesandboxprofile. - Distributed: every image on its own:
FUTURE_AGI_VERSION,FRONTEND_VERSION,AGENTCC_GATEWAY_VERSION,FI_COLLECTOR_VERSION,SERVING_VERSION,CODE_EXECUTOR_VERSIONandSIMULATION_RUNNER_VERSION. WithFI_COLLECTOR_VERSIONunset, Compose builds the collector from your checkout.
On a host with an NVIDIA GPU, pin SERVING_VERSION to vX.Y.Z-gpu, the CUDA build (linux/amd64 only). A plain vX.Y.Z is the CPU build.
Upgrades & rollback covers moving the pins.
Decide how people get accounts
- Email: set
MAILGUN_API_KEY,MAILGUN_SENDER_DOMAINandDEFAULT_FROM_EMAIL, andAPP_URL, which the links in emails are built from. Mailgun is the supported provider; SMTP settings are not read from the environment. - Without email: invites return a link for you to share, and “Forgot password” answers with the command an administrator runs on the host. See Reset a password.
Leave OSS_RETURN_PASSWORD_RESET_LINK unset or false: true lets anyone who can reach the instance take over any account. See Sign-in.
Isolate code evals
Standalone runs Python code evals in an unprivileged sandbox inside the app container, which suits installs where everyone who can write a code eval is trusted with the data. If they must not trust one another, set COMPOSE_PROFILES=sandbox in .env and run ./bin/install again. Distributed always uses the nsjail code-executor. Details, and what each option needs, are in Isolate the code executor.
Test a restore
Back up Postgres, ClickHouse and object storage, and restore the backup once as a test before you rely on it. Backups & restore has the commands.
Start Distributed with the production overlay
deploy/docker-compose.production.yml layers on docker-compose.distributed.yml. It sets ENV_TYPE=production and re-binds each required value with ${VAR:?}, so Compose refuses to start with must be set for production and names the missing variable. It requires:
SECRET_KEY,AGENTCC_INTERNAL_API_KEY,AGENTCC_ADMIN_TOKEN,PG_PASSWORDandMINIO_ROOT_PASSWORDPROPERTY_CATALOG_API_PASSWORDandPROPERTY_CATALOG_CONSUMER_PASSWORD, which match the observed-attribute catalog’s ClickHouse usersFRONTEND_URLandVITE_HOST_API- Every image version:
FUTURE_AGI_VERSION,FRONTEND_VERSION,AGENTCC_GATEWAY_VERSION,FI_COLLECTOR_VERSION,SERVING_VERSION,CODE_EXECUTOR_VERSIONandSIMULATION_RUNNER_VERSION
Run every docker compose command for this install with the overlay’s env file and files, for example:
docker compose --env-file deploy/.env.production \
-f docker-compose.distributed.yml -f deploy/docker-compose.production.yml ps
A plain docker compose command reads the Standalone docker-compose.yml under the same project name, and ./bin/install ignores the overlay. Upgrade with the production overlay has the upgrade steps.
deploy/setup.sh writes deploy/.env.production and starts the stack. A first install runs:
Write deploy/.env.production
./deploy/setup.sh --skip-up # write deploy/.env.production, start nothingIt asks for the public URLs, every image version and the two catalog passwords, and generates SECRET_KEY, AGENTCC_INTERNAL_API_KEY, AGENTCC_ADMIN_TOKEN, PG_PASSWORD and MINIO_ROOT_PASSWORD.
Add the secrets setup.sh leaves out
deploy/setup.sh writes no CH_PASSWORD or AGENTCC_WEBHOOK_SECRET, and an empty INTEGRATION_ENCRYPTION_KEY. Without them ClickHouse’s default user has no password, the app refuses the gateway’s request logs, and connecting an integration fails. Add them before the initialization in the next step: PeerDB’s ch_dest peer is created with CH_PASSWORD then and is not updated afterwards.
echo "CH_PASSWORD=$(openssl rand -hex 32)" >> deploy/.env.production
echo "AGENTCC_WEBHOOK_SECRET=$(openssl rand -hex 32)" >> deploy/.env.productionThen replace the empty INTEGRATION_ENCRYPTION_KEY="" line in deploy/.env.production with a key from the Fernet command in Replace published defaults with real secrets. A CH_PASSWORD you choose yourself must not contain <, > or &.
Initialize the databases
The overlay’s start-up jobs only check the database schema, the ClickHouse objects and the PeerDB mirrors; they never create them. Run the initialization jobs once by hand, as deploy/README.md describes. That guide also covers the manual flow, deployment topologies and resource sizing.
Start the stack
./deploy/setup.sh --confirm-initialized # pull the images and start the stack Warning
PG_PASSWORD is written into the Postgres volume on the first start, so set it before you start the stack for the first time. deploy/setup.sh never regenerates a value in an existing deploy/.env.production: for retained data, reuse the credentials the data was created with.
Which file a key goes in
--env-file deploy/.env.production fills only the ${VAR} references in the compose files. The backend, the workers, the LLM gateway and serving also load the root .env as env_file, and a key the compose files set wins over it. So:
| File | Keys |
|---|---|
deploy/.env.production | Every key the compose files reference: the required keys above, CH_PASSWORD, AGENTCC_WEBHOOK_SECRET, INTEGRATION_ENCRYPTION_KEY, APP_URL, MINIO_URL, FI_COLLECTOR_PUBLIC_URL, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, the AWS_* keys, the Mailgun keys, FUTURE_AGI_TELEMETRY_DISABLED |
Root .env | Every other key, among them BASE_URL, ALLOWED_HOSTS, CORS_ALLOWED_ORIGINS, EXTRA_CSRF_ORIGINS and UPLOAD_BUCKET_NAME. In deploy/.env.production they have no effect, and nothing reports it. |
To check a key, search the compose files for it. No output means it goes in the root .env:
grep -F '{BASE_URL:' docker-compose.distributed.yml deploy/docker-compose.production.yml
Whatever the root .env holds still reaches those containers. Check it for stale values, such as OSS_RETURN_PASSWORD_RESET_LINK=true from an older .env.example.
Move to managed data stores
Compose-managed Postgres, ClickHouse, Redis and object storage suit a single host. On Kubernetes, external datastores are the Helm chart’s default: see Install for production.
Under Compose, the repository documents these overrides:
-
Postgres and ClickHouse: the compose files name the bundled
postgresandclickhouseservices, and their ports, inenvironment:blocks, which win over.env, so values in.envdo not reach them. Override every such value, for every service that sets it, in adocker-compose.override.yml. Besides the app’sPG_HOST,PGBOUNCER_HOSTandCH_HOST, that includes the collector’sFI_CH_URLand its Postgres settings (FI_PG_WRITEandFI_PG_READon Standalone,FI_PG_WRITE_HOSTandFI_PG_READ_HOSTon Distributed), the observed-attribute catalog’sPROPERTY_CATALOG_CH_HOSTandFI_OBSERVED_CATALOG_CH_URL, and on Distributed Temporal’sPOSTGRES_SEEDS,peerdb-init’sSRC_PG_HOSTandDST_CH_HOST, and the catalog bootstrap’sCLICKHOUSE_HOST.FI_CH_URLmust name the same ClickHouse asCH_HOST: otherwise the collector writes spans to one while the app reads the other, and traces never appear. To list every line that names them:docker compose config | grep -nE 'postgres|clickhouse' -
Object storage:
STORAGE_BACKENDtakesminio(the default),s3orgcs.s3needsS3_ENDPOINT_URL,S3_ACCESS_KEYandS3_SECRET_KEY, andgcsneedsGCS_HMAC_ACCESS_KEYandGCS_HMAC_SECRET_KEY, set in a compose override. -
Redis: Standalone runs its Redis inside the
appcontainer, with nothing in it that must survive a restart. Distributed’s services find Redis at addressesdocker-compose.distributed.ymlsets. If you move it, the backend’sREDIS_URLand the collector’sFI_AUTH_REDIS_ADDRmust name the same server: a mismatch fails silently, and key revocation stops reaching the collector.
On Distributed, .env sets COMPOSE_FILE, which turns off Compose’s automatic loading of docker-compose.override.yml. List the override there too:
COMPOSE_FILE=docker-compose.distributed.yml:docker-compose.override.yml
The Helm chart states what each managed service must support, and Compose runs the same application. On Distributed, PeerDB’s sync also reads Postgres through logical replication (its bundled Postgres runs with wal_level=logical), which Helm’s outbox sync does not need.
| Datastore | Supported | Not supported |
|---|---|---|
| PostgreSQL | 16 (11 or newer works), a direct connection, password authentication. The chart’s cloud examples use RDS for PostgreSQL and Cloud SQL | IAM database authentication |
| ClickHouse | 25.3 or newer, single node, plain HTTP and native ports, with the tiered storage policy | TLS-only endpoints such as ClickHouse Cloud; replicated clusters |
| Redis | 6 or newer. Managed Redis (ElastiCache, Memorystore, Azure Cache) is expected to work but not tested in CI | TLS for the LLM gateway and the collector |
| Object storage | AWS S3 or an S3-compatible service with access keys. GCS through HMAC keys is expected to work but not tested in CI | IAM roles for service accounts, Workload Identity, Entra ID; Azure Blob |
Note
Distributed’s code-executor and Standalone’s sandbox profile run with privileged: true, which ECS Fargate, Cloud Run and some Kubernetes policies refuse. Run them where privileged containers are allowed, such as EC2, GKE with privileged containers enabled, or bare metal. Standalone’s built-in sandbox needs no privileges. The platform matrix is in Requirements.
Dive deeper
Questions & Discussion