datris doctor checks a running deployment for the failure modes that are cheap to detect and expensive to miss: a Vault token about to expire, an AI slot secret missing so the server won’t boot after an upgrade, an embedding model that was never pulled, a disk about to fill, data sitting on an anonymous Docker volume, a container still running with an old .env, a mixed-version stack. Every finding names the command that fixes it. Doctor never changes anything.
Where it runs
The server-side report is also available at
GET /api/v1/doctor for scripts. It is not a public endpoint — it names environment keys, Vault paths and model ids — so an API key with config:read is required when API keys are on.
The checks
Checks below the line run only from the CLI on the machine running Docker; the UI and MCP surfaces report them as not applicable. Secret values never appear in any output — only key names.
CLI
docker-compose.yml and .env, or point it there with --project-dir (and --compose-file for the standalone file). Unlike the other CLI commands, doctor talks to the server directly rather than through the MCP server — a dead MCP server is itself a finding. Set DATRIS_URL if the server is not on http://localhost:8080. When API keys are on, set DATRIS_API_KEY — the same variable every other CLI command reads.
Exit codes make it scriptable:
Skipped checks (Docker not on this machine, a service not running, datris.ai not reachable) never affect the exit code. A default install, with the governance controls still off, or a deployment that is behind the latest release, exits
1 because of the governance.controls or version.update warning; switch the controls on and upgrade (or treat 1 as passing) if a script gates on exit 0.
Update check
A fulldatris doctor run (not --pre-upgrade) makes one request that leaves the machine, so the version.update row can tell you whether a newer version exists. It is the only check that does. The request is:
os is linux, darwin or windows; arch is the CPU architecture as the OS names it (x86_64, arm64, aarch64). Nothing else is sent: no API key, cookie, body, hostname or identifier. The answer is {"latest": "1.45.0"}; anything else, a redirect included (it is not followed), is treated as unreachable. It times out after 3 s, and failure is a skip that never changes the exit code. Nothing is written to disk. The server itself never makes this request, and neither does the periodic run, the UI, the run_doctor tool or the installer.
To switch it off, add DATRIS_UPDATE_CHECK=0 to the project .env (doctor reads it from --project-dir) or set it in the environment; 0, false and off all work, and the environment wins over .env. The row then reads “off (DATRIS_UPDATE_CHECK=0)” and nothing is sent. See Security Architecture.
Before an upgrade
--remove-orphans could drop it. When nothing is at error level it prints the upgrade command. See Upgrading.
The installer runs it for you. When you upgrade by re-running curl -fsSL https://get.datris.ai/install.sh | sh and the datris CLI is on the machine, the installer runs the pre-upgrade check before it pulls anything. A warning is printed and the upgrade continues; an error stops the upgrade with the fix on screen. DATRIS_SKIP_DOCTOR=1 bypasses it. Without the CLI the installer says so and continues as it always has.
Periodic run
The boot-time lines catch a problem once, at start. To keep catching them — a Vault token that will expire next month, a disk crossing 85 %, an embedding service that stopped answering — set an interval and the server re-runs the full server-side report on that cadence:DOCTOR lines. When a check flips to error, or recovers from one, the server POSTs to the incident webhook (the same one the recovery agent uses, so one receiver handles both):
event is doctor_error or doctor_recovered. Warnings and checks that stay in error are not re-posted; an error already present when the timer starts is posted once. The AI probes never run on the timer. The interval is 0 (off) by default; the timer polls once a minute, so the first run lands within a minute of boot. Each run logs one DOCTOR periodic run summary line; a POST that fails logs a DOCTOR webhook POST failed warning. Like every outbound call the server makes, the POST is refused when the receiver resolves to a private or loopback address unless DATRIS_ALLOW_PRIVATE_EGRESS=true (see Configuration Reference).
In the recovery agent
When the recovery agent diagnoses an incident,run_doctor is among its read-only tools. Its instructions tell it to call the quick subset once, without AI probes, only when the failure looks like the platform rather than the tap — a missing secret, a model or embedding service not answering, a full disk, a version mismatch — and, if the doctor reports an error, to hand the incident to a human with the doctor’s remediation rather than propose a re-run that will fail the same way. When API keys are on, the agent’s recovery-agent key is granted config:read for this on the first start after upgrading; the grant is audited.
Report format
All surfaces produce the same JSON:status is one of ok, warn, error, skip; surface is server, host, or cli.
Turning off the boot-time lines
The startup subset only logs; it never blocks a start. To silence it, setDOCTOR_ONSTARTUP=false in .env. The on-demand surfaces and the periodic run are unaffected.