Self-hosting
Configuration
AgentsVille is configured entirely through environment variables. This page walks
through every one of them in the order .env.example lists them, says what it
does, and says what happens if you get it wrong.
One file, at the root
There is exactly one .env in the repository, at the root, and every app and
package reads that file and no other — the API server, the web app, the
migration runner and the test runners alike. There is no per-package env file.
Nothing depends on the dotenv package. Node loads the file natively, and
packages/env/src/load-root-env.ts finds the repository root by walking up from
whichever file asked, rather than counting directories, because the server is
bundled to a different depth than it is written at.
A variable already present in the environment always wins over a line in the
file. That is what lets docker-compose.yml's environment: blocks override
.env regardless of load order — and it is also why a stale value exported in
your shell can quietly beat the file you just edited.
Start from the example:
Most values are validated at boot. A missing or too-short one stops the server immediately and names the variable, rather than failing later on a Player's first Message.
Datastores
DATABASE_URL — the MySQL connection string, for example
mysql://user:password@127.0.0.1:3306/agentsville. Required.
Under Compose this is overridden: docker-compose.yml sets it to
mysql://user:${MYSQL_PASSWORD}@mysql:3306/agentsville in the server service, so
the value in .env is what host-side tooling and pnpm db:migrate use, not
what the deployed server uses.
VALKEY_URL — for example redis://127.0.0.1:6379/0. Required, and the
server refuses to boot without it. Rate limits, pending Connector
authorizations and cached session lookups live here.
It is required rather than optional on purpose: the rate limiter in front of
SIGNUP_INVITE_CODE cannot silently fall back to per-process counting and still
be the gate it claims to be. Compose overrides this too, to
redis://valkey:6379/0 — database 0. The test suite uses database 1, so a
misconfigured URL in a test run cannot flush the real one.
Auth and access
BETTER_AUTH_SECRET — minimum 32 characters, validated at boot.
Generate one with openssl rand -base64 48.
This is the most consequential value in the file. It signs sessions, and a key derived from it encrypts every MCP Server header and Connector token at rest. Changing it signs every Player out and makes every stored credential unreadable. Read Rotating secrets before you touch it.
BETTER_AUTH_URL — must be a valid URL. The API's own public origin, for
example https://api.example.com. In development it is
http://localhost:8080.
CORS_ORIGIN — one URL or a comma-separated list. The web origins the
API accepts browser requests from: your own web app, for example
https://app.example.com, plus any hosted AgentsVille UI your Players connect from,
for example https://agentsville.codebrewerz.com. Set it wrong and Players see
requests fail in the browser console rather than a useful error. See
Connecting to a Village.
SIGNUP_INVITE_CODE — minimum 16 characters, validated at boot. The
shared secret a person must present to register. Generate one with
openssl rand -base64 24.
It is never stored against a Player. It is one row, env, among the Operator's
invite codes: it works without limit until the Operator caps it in Settings, it
cannot be deleted, and setting its limit to 0 turns it off. Changing the value
and restarting stops the old one working for everyone at once and affects nobody
who has already registered. Registration is rate limited to 5 attempts per
minute per client address, which is what makes a 16-character secret
sufficient.
Server runtime
NODE_ENV — development, production, or test; defaults to
development. Set it to production on a real deployment. Compose sets it to
production for the server, web and sandbox-broker services regardless.
PORT — the port the API server listens on. Defaults to 8080, and Compose
sets it to 8080 explicitly.
Providers
ANTHROPIC_API_KEY and OPENAI_API_KEY — keys for the two built-in
Providers, both optional. Every other Provider — Ollama, LM Studio, vLLM, an
OpenAI-compatible gateway — is added in Settings → Providers and stored in
the database, its key encrypted with a key derived from BETTER_AUTH_SECRET. A
key saved in Settings for a built-in Provider takes precedence over the .env
one. Memory embeddings use the model chosen in Settings → Memory
embeddings — a local one such as Ollama's nomic-embed-text needs no key at
all. With nothing chosen they fall back to OpenAI when OPENAI_API_KEY is set,
and otherwise Memory is ranked by keyword.
The Provider pickers mark Providers that have no key (or, for an
OpenAI-compatible one, no address) as not set up, so an NPC cannot be pointed at
a Model that could never run. An NPC whose Provider loses its key fails with
PROVIDER_NOT_CONFIGURED rather than erroring cryptically. See
Using local Models.
Set a spend cap in the Provider's console. AgentsVille records Usage and never enforces a quota.
Observability
LOG_DRAIN — a comma-separated list of destination names, empty by
default. The accepted names are axiom, better-stack, clickhouse,
datadog, fs, hyperdx, loki, memory, otlp, posthog and sentry.
Empty means stdout only in production, and a filesystem drain in development. An unrecognised name makes the server refuse to boot, deliberately — a typo that silently ships logs nowhere is only ever discovered mid-incident.
Vendor credentials are deliberately not declared in AgentsVille's schema. Each
adapter reads its own, for instance AXIOM_API_KEY and AXIOM_DATASET, so
adding a destination never means a code change. See
Logs and observability.
Web and SSR URLs
VITE_SERVER_URL — where the browser bundle sends API requests. Must be a
valid URL.
This one is baked into the bundle at build time, not read at runtime.
Changing the API hostname is a rebuild, not a restart. Under Compose you do not
set this directly: the web image takes it as a build argument from
PUBLIC_API_URL.
VITE_SITE_URL — the public site's origin, also baked in at build time.
Leave it unset on your own instance. Unset, every page asks search engines not
to index it and robots.txt disallows crawling, because a self-hosted Village's
landing page and docs are copies of the public ones and its app is private. Set,
the build emits canonical links, absolute share-image URLs and a sitemap.xml
for that origin.
SERVER_URL — where the web app resolves sessions during server-side
rendering. Leave it unset for local development. docker-compose.yml sets it to
http://server:8080, so server-side rendering reaches the API over the Compose
network instead of hairpinning out through public DNS and back in through the
TLS edge.
Edge and proxy
TRUSTED_PROXIES — which upstreams may set X-Forwarded-For. Defaults to
true. Three forms are accepted:
truetrusts the immediate hop. Correct behind the bundled Caddy profile, where nothing else can reach the application ports.falseignores the header entirely, so the client address is the socket address.- A comma-separated list of addresses or CIDRs, for example
10.0.0.0/8,127.0.0.1. This is what you want behind your own proxy.
Anything else — a value that is neither of the keywords nor a parseable list —
throws at boot rather than silently widening back to true.
Rate limits are keyed on the client address. Leaving this at true while the
application port is reachable by anything other than your own proxy hands a
caller a fresh identity per request, including on the route protecting the
Invite Code.
PUBLIC_APP_HOST, PUBLIC_API_HOST — the deployment hostnames, read by
docker-compose.yml to configure Caddy's routing. They default to
app.localhost and api.localhost.
PUBLIC_API_URL — the API's public origin, for example
https://api.example.com. Compose passes it to the web image as the build-time
VITE_SERVER_URL. The API server also reads it itself, as the base of every
Connector's OAuth redirect URI; when it is unset the server falls back to
http://localhost:<PORT>, which no real Provider will accept as a redirect.
These three have localhost defaults because Compose interpolates the whole file
even for pnpm db:start, so nothing fails loudly when you forget them in
production. Caddy simply never gets a certificate and the browser bundle points
at localhost. Confirm them before the first deploy:
Connector OAuth
AgentsVille cannot ship OAuth credentials for somebody else's service, so each
Connector that uses OAuth reads its own pair, named
CONNECTOR_<KIND>_CLIENT_ID and CONNECTOR_<KIND>_CLIENT_SECRET. The kinds
that use OAuth today are linear, notion and atlassian:
<kind> is the catalogue key in packages/ai/src/connectors.ts. These names
are read from the environment by name rather than declared in the schema, so
adding a catalogue entry needs no schema edit — which also means a typo in one
of them is not caught at boot. A Connector with no client configured is shown as
needing an Operator rather than as a button that fails.
Register the redirect URI with each service as:
The prefix itself is configurable through CONNECTOR_OAUTH_PREFIX, which
defaults to CONNECTOR_. There is no reason to change it.
Connectors that use a token rather than OAuth — composio and sentry today —
need nothing from you. A Player pastes the token in.
Sandboxes
These are only used with docker compose --profile sandbox, which runs the one
service that holds the Docker socket. Read
ADR 0006 and
Enabling sandboxes first.
SANDBOX_BROKER_URL — where the API server reaches the broker. Defaults to
empty in the schema; .env.example and docker-compose.yml both use
http://sandbox-broker:8008, the Compose-network address.
SANDBOX_BROKER_TOKEN — the bearer token the API presents to the broker.
The broker refuses to start unless this is at least 16 characters. Generate
one with openssl rand -base64 24.
The API server treats Sandboxes as enabled only when both the URL and the token are non-empty. With either missing, Sandbox Tools report that the feature is not enabled instead of failing with a connection error nobody can act on.
SANDBOX_IDLE_MINUTES — how long a Sandbox may sit unused before it is
stopped. Defaults to 30. Stopped, not destroyed: the filesystem survives, so the
next Message resumes where the NPC left off.
docker-compose.yml also interpolates AGENTSVILLE_SANDBOX_IMAGE, defaulting to
agentsville-sandbox:latest — the image pnpm sandbox:build produces.
Compose-only
These two are read by docker-compose.yml and never by the application:
MYSQL_PASSWORD — the password for the user account the server connects
as. Defaults to password, which keeps pnpm db:start working locally and is
not acceptable on a deployment anybody else can reach.
MYSQL_ROOT_PASSWORD — the MySQL root password, same default and the same
caveat. scripts/backup.sh also reads this one from its own environment.
Set both to real values before the first up. Changing MYSQL_PASSWORD after
the database volume exists means changing it inside MySQL as well; changing only
the variable locks the server out of its own database.
Next
- Environment variables — the same list as a reference table.
- Reverse proxy — the edge contract these URLs assume.
- Troubleshooting — what a wrong value looks like from the outside.