Getting Started
Quickstart
Clone Mirobody, run ./deploy.sh, sign in at localhost:18060, and add an LLM key.
One path from an empty directory to a signed-in Mirobody: clone, run ./deploy.sh, open the browser, sign in with the demo account, then add an LLM key. Everything runs in Docker, so you do not need Python or Node on the host for this page.
Prerequisites
Section titled “Prerequisites”| Requirement | Why |
|---|---|
| Docker + Docker Compose | deploy.sh builds one image and starts four containers. |
| Git | To clone the repository. |
| Git LFS | Terminology and indicator resources under mirobody/res/ are LFS objects. Without LFS you get pointer files and startup fails. |
1. Clone the repository
Section titled “1. Clone the repository”Install Git LFS before cloning: apt install git-lfs, brew install git-lfs, or bundled with Git for Windows.
git lfs install # once per machinegit clone https://github.com/thetahealth/mirobody.gitcd mirobody2. Run the deploy script
Section titled “2. Run the deploy script”./deploy.shThe script does four things:
Writes .env
Sets ENV=localdb and generates 32-character CONFIG_ENCRYPTION_KEY and LOG_ENCRYPTION_KEY values. Existing files are left alone.
Writes config.localdb.yaml
Your override file, seeded with a random JWT_KEY and commented-out placeholders for the LLM keys — OPENROUTER_API_KEY first, DASHSCOPE_API_KEY as the fallback.
Builds the image
An Ubuntu 24.04 image with a Python virtualenv (no Node.js — the web client ships prebuilt). If hub.docker.com is unreachable the script falls back to the docker.1ms.run mirror for every image, and PIP_INDEX_URL lets you point pip at a mirror too.
Starts the stack
Checks that ports 18060 / 18062 / 18069 and the 10.108.0.0/24 subnet are free (a conflict is named and the script refuses, rather than stopping someone else’s containers), then docker compose up -d and tails the logs. Four containers come up: pg (18062), redis (18069), mirobody (18060), and mirobody_worker.
3. Open the web client
Section titled “3. Open the web client”Open http://localhost:18060. The engine serves a prebuilt web client from the frontend/ directory next to the process; it lives outside the Python package on purpose, so a wheel ships the engine rather than 8 MB of JavaScript.
4. Sign in
Section titled “4. Sign in”The config.yaml template ships one predefined account, and deploy.sh deliberately adds none of its own — so the same sign-in works however you started the server. Enter the address, then the code as the verification code:
caregiver@mirobody.ai111111To turn the account off by hand, deleting a block from config.localdb.yaml is not enough: the template’s entry loads first, and config.{env}.yaml replaces a top-level key rather than merging into it. Override the key with an empty value instead:
EMAIL_PREDEFINE_CODES:Password sign-in
Section titled “Password sign-in”Verification codes need Mandrill or SMTP, which a deployment you cloned to try out does not
have — so the sign-in page opens on password and keeps the email code as a third tab. The
API underneath takes an email (or a username) and a password of eight characters or more:
curl -X POST localhost:18060/password/register -H 'Content-Type: application/json' \ -d '{"email":"you@example.com","password":"at-least-8-chars"}'That returns a token and creates the account; POST /password/login with the same body signs
you back in. The hash is bcrypt computed inside Postgres by pgcrypto, so no password is
hashed, compared or logged in Python, and no default password ships in the repository.
register refuses an account that already has a password rather than overwriting it, and a
wrong password answers identically to an unknown account — both are deliberate.
The seeded demo record
Section titled “The seeded demo record”compose.yaml sets SEED_DEMO_DATA=true, so this path arrives with data instead of an empty
database, in two layers. One synthetic person — Demo (synthetic) — is shared into the care
circle of every predefined account, with 244 indicators across two years and five documents
the agent can read. And the account you sign in as owns a thin record of its own — a few weeks
of vitals, one normal checkup, one stored document — so the split between your data and a
record shared with you is visible on screen from the first minute.
Ask about the shared record first, on the Ask page: “What was her latest LDL cholesterol and
how does it compare to a year earlier?” The answer comes from her seeded history, and the
agent tends to point out that the most recent panel is over a year old. Her next panel is
held out of the seed on purpose and ships as a file, mirobody/demo/lab_report_2025-10-15.pdf.
Upload it on the Data page (or with + in Ask) and the two first stages run on a real document: twelve analytes come out with their units, each resolves to a code, and the LDL series gains a fourth point — ask the same question again and the answer moves.
Every value is synthetic and the PDF is stamped as such. The fixture is vendored, so seeding
needs no network and no API key, and it is an upsert, so restarts do not duplicate it. Set
SEED_DEMO_DATA=false for a deployment that will hold real data.
5. Add an LLM key
Section titled “5. Add an LLM key”You are signed in, but the agent has no model to call yet. Edit config.localdb.yaml and set one key — chat, vision (image and PDF reading) and embeddings all follow whichever key exists:
# One key runs everything. OpenRouter is the recommended default;# DashScope is the drop-in fallback where openrouter.ai is unreachable.OPENROUTER_API_KEY: 'sk-or-...'# DASHSCOPE_API_KEY: 'sk-...'Then restart the two application containers:
docker compose restart mirobody mirobody_workerWhich models each agent may use is set per agent under PROVIDERS_DEEP; the entries shipped in config.yaml already reference the key names above, and the chat default follows the key too — claude-sonnet through OpenRouter, qwen when DASHSCOPE_API_KEY is the only key present. Direct keys (GOOGLE_API_KEY, OPENAI_API_KEY) work as well for deployments that prefer one provider.
Embeddings follow the same key
Section titled “Embeddings follow the same key”Free-text indicator search and tidied display names run on embeddings, and they need no
second key: EMBEDDING_PROVIDER is unset by default and follows whichever key exists —
OpenRouter, then DashScope, then Google. Set it explicitly only to pin one:
# EMBEDDING_PROVIDER: openrouter # qwen/qwen3-embedding-8b# EMBEDDING_PROVIDER: qwen # DashScope text-embedding-v4# EMBEDDING_PROVIDER: gemini # gemini-embedding-001With no key at all, browsing the record and asking about indicators by name still work:
those paths are plain SQL. Fuzzy keyword search degrades stepwise instead of failing — a
lexical, offline match first, and the catalogue of what the account actually has when nothing
matches — and indicator names stay in the source’s own spelling
(AlanineAminotransferase-ALT) rather than the display names the embedding pass produces.
Switching providers later means re-embedding (python -m mirobody.indicator embed); when an
embedding key is configured but not working, the failure is quiet —
docker compose logs mirobody_worker is where it says so.
6. Verify the install
Section titled “6. Verify the install”# All four containers up?docker compose ps
# Liveness — returns JSON with the service name, version, and counts of# tools, resources and agents that were discovered at startup.curl http://localhost:18060/api/health
# MCP discovery — JSON-RPC 2.0 over POST, lists the registered tools.curl -X POST http://localhost:18060/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'If /api/health reports "tools": 0, the tool directories were not scanned; check MCP_TOOL_DIRS and the startup log (docker compose logs mirobody).
Next steps
Section titled “Next steps”The three config layers, the key groups, and what is encrypted.
Local Python development, the PyPI package, and the repository layout.
How the server, worker, agents and Pulse fit together.
Point Claude Desktop, Cursor or any MCP client at this server.