Self-hosting the Manthana server
Onboarding a team is just “every laptop points at one server URL” — that URL is baked into the invite. The only real decision is where that one server runs, with HTTPS in front of it.
That last part is not optional. The team token is a bearer credential:
whoever holds it can push as that engineer. It must never travel unencrypted.
manthana-server serve prints a warning if you bind a public address without TLS.
Jargon, once
Section titled “Jargon, once”- Loopback /
127.0.0.1— “this machine only”. Invisible to every other laptop. The default. 0.0.0.0— “accept connections from other machines”. Needed to serve a team, safe only behind HTTPS.- Domain / DNS — a name like
manthana.acme.com. You point an “A record” at your server’s IP at your registrar. - TLS / HTTPS — encryption on the wire.
- Reverse proxy — a front door that terminates HTTPS and forwards to your app. Caddy fetches and renews a free Let’s Encrypt certificate for you with almost no config.
- Tailscale — a private VPN giving each machine a stable
.ts.netname with HTTPS built in. No domain, no open ports.
Pick a path
Section titled “Pick a path”| Path | Best for | HTTPS from | Data store |
|---|---|---|---|
| A. Tailscale | fastest secure pilot | Tailscale | SQLite + in-memory |
| B. Domain + Caddy, no Docker | a small real deployment | Let’s Encrypt | SQLite + in-memory |
| C. Full Docker stack | the productized path | Let’s Encrypt | Postgres + MinIO/S3 |
| D. Kubernetes | you already run k8s | your ingress | external Postgres + S3 |
Paths A and B store raw transcripts in memory, which is fine for a pilot and wrong for anything you care about — they vanish on restart. Move to C or D before you rely on them.
Install the server
Section titled “Install the server”No repo checkout needed:
curl -LsSf https://github.com/Jarus77/manthana/releases/latest/download/install.sh | sh -s serverThen manthana-server init . writes the deploy files (Caddyfile,
docker-compose.yml, docker-compose.tls.yml, .env.example) into the current
directory.
Path A — Tailscale
Section titled “Path A — Tailscale”Everyone installs Tailscale (tailscale up) and joins your tailnet, with MagicDNS
and HTTPS enabled in the Tailscale admin console. Then:
manthana-server serve --tailscale# → runs `tailscale serve` in front of loopback and prints# https://<machine>.<tailnet>.ts.net
manthana-server enroll acme platform --open \ --server-url https://<machine>.<tailnet>.ts.netNothing is exposed to the public internet, and there is no certificate to manage.
Stop sharing with tailscale serve --https=443 off.
Path B — your domain, Caddy, no Docker
Section titled “Path B — your domain, Caddy, no Docker”Point manthana.acme.com’s DNS A record at the VM and open ports 80 and 443.
manthana-server init . # writes ./Caddyfile# edit <your-domain> in ./Caddyfile
manthana-server serve --public-url https://manthana.acme.com & # 127.0.0.1:8000caddy run --config ./Caddyfile
manthana-server enroll acme platform --open --server-url https://manthana.acme.com--public-url doesn’t route anything — it’s what the server prints in shareable
links, so a wrong value gives you a bad link, never a security hole.
Set MANTHANA_SERVER_COOKIE_SECURE=1 once you’re behind TLS, or console logins
won’t stick correctly.
Path C — the full Docker stack
Section titled “Path C — the full Docker stack”Server + Postgres + MinIO (S3) + bucket creation + the wiki client, one command.
manthana-server init . # or clone the repocp .env.example .env # then fill it in — see "Secrets" belowdocker compose up -d # builds server + web, starts pg + minio + bucketdocker compose ps # server should become healthy (/readyz)| Service | Port | What |
|---|---|---|
server |
8000 |
API, founder console at /ui |
web |
3000 |
The Next.js wiki client |
postgres |
5433 (host) |
pgvector-enabled Postgres |
minio |
9000 / 9001 |
S3-compatible object store; console at :9001 (manthana / manthana-secret) |
The server container reaches Postgres and MinIO by service name
(postgres:5432, minio:9000); Compose wires that automatically and it
overrides whatever your .env says for DB and object store. Host ports are for
your machine. Tables are created on startup, idempotently.
The web service needs a reverse proxy in front of it. Hitting
localhost:3000 directly works for a look around, but every API call it makes
will 404 — the client and the server must share one origin. That’s what
deploy/Caddyfile is for, and why it routes by path.
Read the wiki client before you deploy this; it’s the piece most
easily got wrong.
Deploy a pinned release instead of building
Section titled “Deploy a pinned release instead of building”MANTHANA_VERSION=0.6.3 docker compose \ -f docker-compose.yml -f docker-compose.prod.yml pullMANTHANA_VERSION=0.6.3 docker compose \ -f docker-compose.yml -f docker-compose.prod.yml up -dThe overlay swaps the server’s source build for the published image and inherits Postgres and MinIO from the base file. Requires Docker Compose v2.24+.
Running a fork or a mirror? Override
MANTHANA_IMAGErather than editing the Compose file:MANTHANA_IMAGE=ghcr.io/your-org/manthana-server. The default isghcr.io/jarus77/manthana-server, which is what.github/workflows/publish-image.ymlactually pushes. Always setMANTHANA_VERSIONexplicitly for anything real — the default tracks the current release and will move under you.
Add HTTPS
Section titled “Add HTTPS”MANTHANA_DOMAIN=manthana.acme.com \ docker compose -f docker-compose.yml -f docker-compose.tls.yml up -dThat overlay runs Caddy on 80/443, fetches and renews a Let’s Encrypt certificate
for $MANTHANA_DOMAIN, and proxies to the server container. Its certificate
lives in the caddy_data volume, so don’t delete it casually.
Note: the TLS overlay’s one-line caddy reverse-proxy sends everything to
server:8000. If you’re also running the web client, use
deploy/Caddyfile instead — it does the path routing
both services need. See the wiki client.
Path D — Kubernetes
Section titled “Path D — Kubernetes”Manifests in deploy/k8s/. The server is stateless;
Postgres and S3 are external or managed — point the ConfigMap at them.
kubectl apply -f deploy/k8s/configmap.yamlkubectl create secret generic manthana-server-secrets \ --from-literal=MANTHANA_SERVER_JWT_SECRET="$(openssl rand -hex 32)" \ --from-literal=MANTHANA_SERVER_ADMIN_TOKEN="$(openssl rand -hex 24)" \ --from-literal=ANTHROPIC_API_KEY="sk-ant-..." # if LLM=anthropickubectl apply -f deploy/k8s/deployment.yaml -f deploy/k8s/service.yamlThe Deployment runs non-root (uid 10001, capabilities dropped) with /healthz
liveness and /readyz readiness probes. Put an Ingress with TLS in front of the
Service. deploy/k8s/secret.example.yaml is a template — prefer the
kubectl create secret form above so secrets never touch a file. Update the
image tag in deployment.yaml; it is pinned to an older release.
Secrets
Section titled “Secrets”The server refuses to start with the shipped dev placeholders or an empty
value. Put real ones in .env and never on a command line — command lines leak
into shell history, process lists, and logs.
MANTHANA_SERVER_JWT_SECRET="$(openssl rand -hex 32)"MANTHANA_SERVER_ADMIN_TOKEN="$(openssl rand -hex 24)"| Variable | Why it matters |
|---|---|
MANTHANA_SERVER_JWT_SECRET |
Signs every agent, founder, and engineer token. Rotating it invalidates all of them at once. |
MANTHANA_SERVER_ADMIN_TOKEN |
Gates the founder console and every admin endpoint. Safe to rotate independently. |
ANTHROPIC_API_KEY |
Only needed with MANTHANA_SERVER_LLM=anthropic |
OPENAI_API_KEY / OPENROUTER_API_KEY |
The equivalents for MANTHANA_SERVER_LLM=openai / openrouter. MANTHANA_SERVER_LLM_API_KEY overrides either. |
If you don’t set the first two, manthana-server serve generates them once and
persists them to ~/.manthana-server/server-secrets.toml (mode 0600) — stable
across restarts, because regenerating would invalidate every issued token. Back
that file up.
Every variable, with defaults, is in Environment variables.
Before you call it done
Section titled “Before you call it done”manthana-server doctorChecks secrets, database, object store, LLM provider and key, k-anon floor, and the budget default. On a Postgres deployment it also flags the two misconfigurations that lose things silently: an in-memory object store (raw transcripts vanish on restart) and non-secure cookies behind TLS (logins don’t stick).
Then, deliberately, decide about the LLM passes — all three are off by default and the server writes no wiki articles until you turn them on. See Privacy posture & budgets.
A note on
MANTHANA_SERVER_LLM=claude_cli. It lets the server use a Claude CLI you’re already logged into instead of an API key, which is excellent for a server you run as yourself on a laptop or a VM — and it does not work in the published container images, which have neither the binary nor a logged-in$HOME. For Docker or Kubernetes, useanthropic,openaioropenrouter. If you misconfigure it the server logs loudly and falls back to the mock rather than crashing, so the symptom is an empty wiki, not an outage.
Pointing the server at a different model
Section titled “Pointing the server at a different model”anthropic is the default recommendation, not the only one. Three others work in
a container:
# OpenAIMANTHANA_SERVER_LLM=openaiOPENAI_API_KEY=sk-…
# OpenRouter — one key in front of hundreds of models, and it reports the real# cost of each call, so per-org spend is exact rather than estimatedMANTHANA_SERVER_LLM=openrouterOPENROUTER_API_KEY=sk-or-…
# Your own endpoint — anything that speaks the OpenAI chat-completions APIMANTHANA_SERVER_LLM=openaiMANTHANA_SERVER_LLM_BASE_URL=http://vllm.internal:8000/v1MANTHANA_SERVER_LLM_API_KEY=whatever-your-server-acceptsopenai and openrouter are one provider over stdlib HTTP — no extra package,
unlike anthropic’s manthana-server[llm]. MANTHANA_SERVER_LLM_API_KEY works
for any of them if you’d rather not use the conventional env var name (handy when
one secret store holds keys for several services).
The self-hosted case is the one worth knowing about. vLLM, Ollama and LM Studio
all implement the same API, so MANTHANA_SERVER_LLM_BASE_URL pointed at one of
them runs every server-side pass against a model on your own hardware — enrichment,
consolidation, project overviews. No third party ever sees a session. In Compose,
put the endpoint on the internal network and it never touches the public internet
at all.
Set the model ids to match.
MANTHANA_SERVER_LLM_MODEL,MANTHANA_SERVER_ENRICH_MODELandMANTHANA_SERVER_CONSOLIDATE_MODELall default to Anthropic ids. Switch provider without switching these and every call fails — silently, because a failed pass degrades to “no data” rather than erroring. OpenRouter ids carry a vendor prefix (openai/gpt-4o-mini); OpenAI’s don’t; a self-hosted server wants whatever name it serves under.manthana-server doctorcatches a leftoverclaude-id and tells you which variable it’s in.
The same degrade-don’t-crash rule applies throughout: a missing key logs a warning
and falls back to the mock, so a misconfigured provider looks like an empty wiki
rather than a broken server. manthana-server doctor reports the active provider,
its endpoint, and whether the key is present — check it after any provider change.
→ The wiki client — the actual UI, and the same-origin constraint → Operations & upgrades → Environment variables