Run the full Manifest stack on your own machine. No Node.js required, just Docker. All three paths end in the same place: a running stack at http://localhost:2099 where you sign up. The first account you create becomes the admin. No demo credentials are pre-seeded.Documentation Index
Fetch the complete documentation index at: https://manifest.build/docs/llms.txt
Use this file to discover all available pages before exploring further.
The bundled compose file binds port 2099 to
127.0.0.1 only, so the dashboard is reachable on the host machine but not over the LAN. See Exposing on the LAN to change this.Installation
- Quick install (recommended)
- Docker Compose
- Docker Run (BYO PostgreSQL)
One command. The installer downloads the compose file into
Useful flags:
~/manifest, generates a secret, and brings up the stack. First boot pulls the app image and Postgres, so give it up to a couple of minutes.Prefer to review the script before running it?
Prefer to review the script before running it?
--dir <path> to install elsewhere, --dry-run to preview, --yes to skip the confirmation prompt.When the installer finishes, open http://localhost:2099 and sign up for an account. Then head to the Routing page to add an LLM provider (OpenAI, Anthropic, Gemini, etc.) with your API key.Verify
After connecting a provider, send a test request and watch it land in the dashboard. Grab your Manifest API key from the dashboard (it starts withmnfst_) and run:
That doesn't look like a Manifest key, you’re still using the placeholder — replace mnfst_YOUR_KEY_HERE with the real key from the dashboard.
Verifying the image signature
Published images are signed with cosign keyless signing (Sigstore). Verify before pulling:Custom port
If port 2099 is taken, change both the mapping andBETTER_AUTH_URL:
docker-compose.yml:
.env:
Upgrading from a pre-2099 install? Your existing stack keeps running on port 3001 with no changes — the backend’s own fallback is still
3001, so the new image works against your old compose file. If you want to refresh your compose file but stay on the legacy port (to avoid reconfiguring OAuth callbacks, reverse proxies, or bookmarks), set PORT=3001 in .env and the bundled compose file will honour it for both the host binding and the internal listener.Exposing on the LAN
By default the compose file binds port 2099 to127.0.0.1 only. The dashboard is reachable from the host but not from other machines on the network. To expose it on the LAN:
Change the port binding
Edit
docker-compose.yml and change the ports line from "127.0.0.1:2099:2099" to "2099:2099".Set BETTER_AUTH_URL
In
.env, set BETTER_AUTH_URL to the host you’ll reach the dashboard on, e.g. http://192.168.1.20:2099 or https://manifest.mydomain.com. This must match the URL in the browser or Better Auth will reject the login with “Invalid origin”.Image tags
Every release is published with the following tags:| Tag | Example | Description |
|---|---|---|
major.minor.patch | 5.46.0 | Fully pinned |
major.minor | 5.46 | Latest patch within a minor |
major | 5 | Latest minor+patch within a major |
latest | — | Latest stable release |
sha-<short> | — | Exact commit for rollback |
linux/amd64 and linux/arm64.
Upgrading
Manifest ships a new image on every release. To upgrade an existing compose install:pgdata volume is preserved across upgrades. Pin to a specific major version (e.g. manifestdotbuild/manifest:5) in docker-compose.yml if you want control over when major upgrades happen.
Backup and persistence
All state lives in thepgdata named volume mounted at /var/lib/postgresql/data in the postgres service. Nothing else in the Manifest container is stateful.
Back up (from the host, with the stack running):
Environment variables
Core| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | Yes | — | PostgreSQL connection string |
BETTER_AUTH_SECRET | Yes | — | Session signing secret (min 32 chars) |
BETTER_AUTH_URL | No | http://localhost:2099 | Public URL. Set this when using a custom port |
PORT | No | 2099 | Internal server port |
NODE_ENV | No | production | Node environment |
SEED_DATA | No | false | Seed demo data on startup |
Additional environment variables
Additional environment variables
Network and security
Rate limiting
Default: 100 requests per 60-second window.LLM proxy
Email alerts (Mailgun)
OAuth providers
| Variable | Default | Description |
|---|---|---|
BIND_ADDRESS | 127.0.0.1 | Bind address |
CORS_ORIGIN | — | Allowed CORS origin |
API_KEY | — | Internal API key |
| Variable | Default | Description |
|---|---|---|
THROTTLE_TTL | 60000 | Rate limit window in ms |
THROTTLE_LIMIT | 100 | Max requests per window |
| Variable | Default | Description |
|---|---|---|
PROVIDER_TIMEOUT_MS | 180000 | Per-attempt timeout (ms) for upstream provider requests. Set strictly below your client’s timeout so the fallback chain has room to run. Slow local models may need this raised. Non-numeric, zero, or negative values fall back to the default. |
| Variable | Description |
|---|---|
MAILGUN_API_KEY | Mailgun API key |
MAILGUN_DOMAIN | Mailgun domain |
MAILGUN_FROM | Sender address for alerts |
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Google OAuth |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | GitHub OAuth |
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET | Discord OAuth |
Stop and clean up
Telemetry
Once a day, each install sends us a small anonymous report. That’s how we know whether anyone’s actually using the thing, and which providers are popular enough to deserve more work. It’s aggregates, never content: no prompts, no messages, no keys, nothing tied to a user. Thirteen fields total.What gets sent
| Field | Example | Purpose |
|---|---|---|
schema_version | 1 | So the shape can grow without breaking old clients |
install_id | random UUIDv4 | Count distinct installs. Generated once on first boot, persisted, never rotated |
manifest_version | 5.47.0 | Version adoption across the fleet |
messages_total | 1284 | Daily activity per install |
messages_by_provider | {"anthropic": 700, "openai": 500} | Provider mix. Anything we don’t recognize collapses to "custom", so self-hosted provider names and URLs stay local |
messages_by_tier | {"simple": 800, "standard": 400, ...} | Routing tier usage |
messages_by_auth_type | {"api_key": 1200, "subscription": 84} | API key vs. paid-subscription usage |
tokens_input_total | 1_450_000 | Volume-weighted signal |
tokens_output_total | 890_000 | Same |
agents_total | 4 | Configuration scale |
agents_by_platform | {"openclaw": 3, "hermes": 1} | Which agent clients people use |
platform | linux / darwin / windows | OS distribution |
arch | x64 / arm64 | Architecture distribution |
Never sent
Tenant IDs, user IDs, emails, API keys, prompts, message contents, model names, custom provider URLs, OAuth client IDs, hostnames, raw IPs. The ingest takes a SHA-256 of your IP and throws the original away; we keep the hash so we can rate-limit bad actors without knowing where they actually live.When
- Once every 24 hours, per install.
- The first report is delayed by a random 0–24h offset, so a fleet of containers rebooted together doesn’t all hit the endpoint at the same minute.
- Off by default when
NODE_ENV != production. Dev machines are never going to accidentally send. - If the endpoint is down, we log it and try again on the next hourly tick. Your proxy keeps serving requests — the sender never gets in the way.
Turning it off
Put this in your.env (or docker-compose.yml) and restart the container:
Sending it somewhere else
If you’d rather run your own fleet dashboard, pointTELEMETRY_ENDPOINT at a URL you control: