Skip to content

Latest commit

 

History

History
355 lines (267 loc) · 13.9 KB

File metadata and controls

355 lines (267 loc) · 13.9 KB
title Self-hosted
description Run Manifest on your own machine with Docker. Covers the quick installer, docker-compose, bringing your own PostgreSQL, signed images, and telemetry opt-out.
icon docker
keywords
self-hosted LLM router
Docker
docker-compose
PostgreSQL
BETTER_AUTH_SECRET
telemetry

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.

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](#exposing-on-the-lan) to change this.

Installation

One command. The installer downloads the compose file into `~/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.
```bash
bash <(curl -sSL https://raw.githubusercontent.com/mnfst/manifest/main/docker/install.sh)
```

<Accordion title="Prefer to review the script before running it?">
  ```bash
  curl -sSLO https://raw.githubusercontent.com/mnfst/manifest/main/docker/install.sh
  less install.sh
  bash install.sh
  ```
</Accordion>

Useful flags: `--dir <path>` to install elsewhere, `--dry-run` to preview, `--yes` to skip the confirmation prompt.

When the installer finishes, open [http://localhost:2099](http://localhost:2099) and sign up for an account. Then head to the [Routing](http://localhost:2099/routing) page to add an LLM provider (OpenAI, Anthropic, Gemini, etc.) with your API key.
Same underlying flow as the install script, but you drive it yourself so you can edit the config before booting the stack.
<Steps>
  <Step title="Download the compose file and the env template">
    ```bash
    curl -O https://raw.githubusercontent.com/mnfst/manifest/main/docker/docker-compose.yml
    curl -O https://raw.githubusercontent.com/mnfst/manifest/main/docker/.env.example
    cp .env.example .env
    ```
  </Step>
  <Step title="Set a real BETTER_AUTH_SECRET">
    Open `.env` in your editor and set `BETTER_AUTH_SECRET` to a random string. You can generate one with:

    ```bash
    openssl rand -hex 32
    ```

    Optional: to use a stronger database password, set both `POSTGRES_PASSWORD` and `DATABASE_URL` in `.env` — they must agree, and any special characters in the password need to be percent-encoded in the URL.
  </Step>
  <Step title="Start the stack">
    ```bash
    docker compose up -d
    ```

    Give it up to a couple of minutes on a cold pull — you can watch startup with `docker compose logs -f manifest`.
  </Step>
  <Step title="Create your admin account">
    Go to [http://localhost:2099](http://localhost:2099) and sign up. The first account you create becomes the admin.
  </Step>
  <Step title="Connect a provider">
    Open the [Routing](http://localhost:2099/routing) page and add an LLM provider (OpenAI, Anthropic, Gemini, etc.) with your API key.
  </Step>
</Steps>

<Warning>
  Before exposing this instance beyond localhost, double-check that `BETTER_AUTH_SECRET` is a real secret (not the placeholder), and if you enable email verification, set `BETTER_AUTH_URL` to a reachable public URL so the verification links resolve.
</Warning>
If you already have a PostgreSQL instance, replace `user`, `pass`, and `host` with your actual database credentials:
```bash
docker run -d \
  -p 2099:2099 \
  -e DATABASE_URL=postgresql://user:pass@host:5432/manifest \
  -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
  -e BETTER_AUTH_URL=http://localhost:2099 \
  manifestdotbuild/manifest
```

<Accordion title="Windows (PowerShell)">
  ```powershell
  $secret = -join ((48..57 + 97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })

  docker run -d `
    -p 2099:2099 `
    -e DATABASE_URL=postgresql://user:pass@host:5432/manifest `
    -e BETTER_AUTH_SECRET=$secret `
    -e BETTER_AUTH_URL=http://localhost:2099 `
    manifestdotbuild/manifest
  ```
</Accordion>

<Accordion title="Windows (CMD)">
  Generate a 64-character hex secret with any tool you trust, then:

  ```cmd
  docker run -d ^
    -p 2099:2099 ^
    -e DATABASE_URL=postgresql://user:pass@host:5432/manifest ^
    -e BETTER_AUTH_SECRET=<your-64-char-secret> ^
    -e BETTER_AUTH_URL=http://localhost:2099 ^
    manifestdotbuild/manifest
  ```
</Accordion>

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 with mnfst_) and run:

curl -X POST http://localhost:2099/v1/chat/completions \
  -H "Authorization: Bearer mnfst_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"model": "manifest/auto", "messages": [{"role": "user", "content": "Hello"}]}'

If the response comes back with 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:

cosign verify manifestdotbuild/manifest:<version> \
  --certificate-identity-regexp="^https://github.com/mnfst/manifest/" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com"

Custom port

If port 2099 is taken, change both the mapping and BETTER_AUTH_URL:

docker run -d \
  -p 8080:2099 \
  -e BETTER_AUTH_URL=http://localhost:8080 \
  ...

Or in docker-compose.yml:

ports:
  - '127.0.0.1:8080:2099'

...and in .env:

BETTER_AUTH_URL=http://localhost:8080

If you see an "Invalid origin" error on the login page, BETTER_AUTH_URL doesn't match the URL you're accessing the dashboard on. The host matters as much as the port.

**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 to 127.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:

Edit `docker-compose.yml` and change the `ports` line from `"127.0.0.1:2099:2099"` to `"2099:2099"`. 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". ```bash docker compose up -d ```

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

Images are built for both linux/amd64 and linux/arm64.

Upgrading

Manifest ships a new image on every release. To upgrade an existing compose install:

docker compose pull
docker compose up -d

Database migrations run automatically on boot, no manual steps. Your data in the 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 the pgdata 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):

docker compose exec -T postgres pg_dump -U manifest manifest > manifest-backup-$(date +%F).sql

Restore into a fresh stack:

docker compose up -d postgres
cat manifest-backup.sql | docker compose exec -T postgres psql -U manifest manifest
docker compose up -d

To list or remove the volume manually:

docker volume ls | grep pgdata
docker compose down -v    # destroys all data

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
**Network and security**
Variable Default Description
BIND_ADDRESS 127.0.0.1 Bind address
CORS_ORIGIN Allowed CORS origin
API_KEY Internal API key

Rate limiting

Variable Default Description
THROTTLE_TTL 60000 Rate limit window in ms
THROTTLE_LIMIT 100 Max requests per window

Default: 100 requests per 60-second window.

LLM proxy

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.

Email alerts (Mailgun)

Variable Description
MAILGUN_API_KEY Mailgun API key
MAILGUN_DOMAIN Mailgun domain
MAILGUN_FROM Sender address for alerts

OAuth providers

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

Full env var reference: github.com/mnfst/manifest

Stop and clean up

docker compose down       # Stop services (keeps data)
docker compose down -v    # Stop and delete all data

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:

MANIFEST_TELEMETRY_DISABLED=1

The sender checks the flag before doing anything else. No database read, no DNS lookup, no request leaves the box.

Sending it somewhere else

If you'd rather run your own fleet dashboard, point TELEMETRY_ENDPOINT at a URL you control:

TELEMETRY_ENDPOINT=https://telemetry.mycompany.internal/v1/report

Docker Hub

The image is available at manifestdotbuild/manifest on Docker Hub.