Getting started
Install with Docker Compose
This is the supported way to run a Village other people will use: one server,
docker compose, and a bundled Caddy that terminates TLS for two hostnames and
issues the certificates itself.
You are the Operator. You hold the Provider keys, you hand out the Invite Code, and you carry the bill.
Before you start
You need:
- A server with Docker and the Compose plugin, and a checkout of the repository on it.
- Two hostnames —
app.example.comandapi.example.comin the examples below. Both must already resolve to the server before the first start, or Caddy cannot issue certificates for them. - Ports 80 and 443 open.
- At least one Provider key.
Both origins have to be HTTPS. The session cookie is SameSite=None; Secure, so
there is no working plain-HTTP deployment, a LAN included.
MySQL publishes on 127.0.0.1:3306 for host-side tooling, so it is not
internet-reachable even with no firewall rule. Set a real password anyway, block
3306 at the firewall if you have one, and reach the database from another
machine over an SSH tunnel rather than by republishing the port.
Create the .env
Everything is configured from one .env at the repository root. Compose reads
it too, for the values it interpolates itself.
Generate the secrets
BETTER_AUTH_SECRET must be at least 32 characters. It signs sessions, and a
key derived from it encrypts stored MCP Server headers and Connector
credentials — so changing it later signs everyone out and makes every stored
credential unreadable. Set it once and keep it.
SIGNUP_INVITE_CODE must be at least 16 characters. It is the Invite Code
every new Player presents to register, and it is never stored against anyone.
There is no per-person invite: you rotate the code by changing this value and
restarting the server, which stops the old one working immediately without
affecting anyone already registered.
Set the hostnames
Add these to .env:
PUBLIC_APP_HOST and PUBLIC_API_HOST are read by Compose and handed to Caddy.
They are the two hostnames it answers on and requests certificates for.
PUBLIC_API_URL is where the browser bundle sends its requests, and where a
Player is sent back after authorising a Connector. It is baked into the bundle
at build time, so changing it later needs a rebuild, not just a restart.
These three fall back to localhost defaults when unset, because Compose interpolates the whole file even when you only want the database. Nothing fails loudly if you forget them in production — Caddy simply never gets a certificate, and the browser bundle points at localhost. Check before the first start:
You do not need to set VALKEY_URL or DATABASE_URL for this deployment.
Compose points the server at the mysql and valkey services directly, and
those values win over the file.
Set the Provider keys
Set ANTHROPIC_API_KEY, OPENAI_API_KEY, both, or neither. With neither,
add a local Provider in Settings → Providers after you sign in; see
Using local models.
Then set a spend cap in the Provider's own console. AgentsVille records the tokens each Message consumed and shows them, but it never enforces a quota. The Provider's cap is the only hard ceiling on what an instance can cost you.
Start it
The --profile caddy part is what runs the bundled edge. Without a profile the
caddy service is left out entirely and nothing terminates TLS. The other
choice is --profile none, for your own nginx, Apache or other proxy in front:
the web app and the API publish on 127.0.0.1:3000 and 127.0.0.1:8080 under
every profile and never on a public interface, so point your proxy at those and
read Reverse proxy for what it has to
satisfy.
Migrations run from the server's start command before it listens. A failed
migration crashloops that service on purpose — a server answering against a
stale schema is worse than one that is down — so check
docker compose logs server before looking anywhere else.
Verify
It answers {"status":"ok","service":"agentsville"} once the server is up and
can reach the database.
Then check the edge is not buffering the reply stream. A buffering proxy does not error: the reply simply arrives all at once at the end, and the product feels broken for reasons nothing logs.
That reads a fixed five-frame stream needing no account and no Provider key, and fails if the frames arrive together or arrived compressed. Run it again after any change to the proxy configuration.
Register — the first Player becomes the Operator
Open https://app.example.com and register with the Invite Code you generated.
Whoever registers first claims operatorship, and everyone after that is an ordinary Player. The claim is atomic, so two simultaneous registrations cannot both win. Register yourself before handing the Invite Code to anyone else.
Operatorship is a row in the database rather than a value in .env, which means
a restore from backup carries it — a restored instance does not silently promote
whoever signs in next. It also means there is no interface for moving it: on an
instance whose first Player has left, moving operatorship is a database update
performed by whoever holds .env, which is the same person. See
the decision record for why it
is one boolean and not a role system.
Being the Operator gets you three things a Player does not have: the Village's default Provider and Model, the ability to register an MCP Server that runs a command on the host, and the ability to enable network egress from a Sandbox.