Skip to content
Get Started

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.

RequirementWhy
Docker + Docker Composedeploy.sh builds one image and starts four containers.
GitTo clone the repository.
Git LFSTerminology and indicator resources under mirobody/res/ are LFS objects. Without LFS you get pointer files and startup fails.

Install Git LFS before cloning: apt install git-lfs, brew install git-lfs, or bundled with Git for Windows.

Terminal window
git lfs install # once per machine
git clone https://github.com/thetahealth/mirobody.git
cd mirobody
Terminal window
./deploy.sh

The 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.

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.

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.ai
111111

To 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:

config.localdb.yaml
EMAIL_PREDEFINE_CODES:

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:

Terminal window
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.

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.

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:

config.localdb.yaml
# 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:

Terminal window
docker compose restart mirobody mirobody_worker

Which 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.

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:

config.localdb.yaml
# EMBEDDING_PROVIDER: openrouter # qwen/qwen3-embedding-8b
# EMBEDDING_PROVIDER: qwen # DashScope text-embedding-v4
# EMBEDDING_PROVIDER: gemini # gemini-embedding-001

With 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.

Terminal window
# 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).