Skip to content

Environment variables

Every setting is read from the environment - a .env file next to the compose file, or your Docker manager’s stack environment. Most can also be changed on the dashboard’s Settings page; a variable set in the environment always wins. Empty is the same as unset.

  • Secret: never shown by value, and never saved from the Settings page.
  • Environment only: read before the database opens, so the Settings page cannot change it.
  • Applies on restart: read once at start; a change takes effect when the doctor restarts.
  • off turns it off: on by default; set off (or none for a list) to disable it.
  • observe - diagnose and notify, never touch anything <- start here
  • suggest - propose an action, wait for you to approve it in the dashboard
  • auto - apply allowed actions without asking

Default: observe · One of: observe, suggest, auto

Poll interval: from the start of one poll to the next.

Default: 30 · Type: whole number

Containers read at once during a poll. Applies on restart.

Default: 6 · Type: whole number · Applies on restart

The Docker endpoint: the socket proxy, never the raw socket. The compose file sets it from DOCTOR_DOCKER_HOST.

Default: tcp://doctor-socket-proxy:2375 · Environment only

True watches only containers labelled doctor.enable=true.

Default: off · Type: on / off

In words, for the diagnosis, the hand-off brief and the Settings page’s labels; neither changes what the doctor does. Both optional. The Docker manager’s name: Portainer, Coolify, Komodo, Dockge…

Default: unset

How a change reaches the host.

Default: unset

What the doctor runs on. auto reads it from the kernel: a Synology says so, and then DSM’s own md0/md1 system arrays are not called degraded. Set synology or generic to override. The name is what diagnoses and the Host page call it; empty says “the host”.

Default: auto · One of: auto, synology, generic

The machine itself. auto tells a Synology NAS from any other Linux by its kernel; synology keeps DSM’s md0/md1 system arrays from reading as degraded and uses its names for drives and pools. The name is what the host is called in diagnoses and on the Host page - empty says “the host”.

Default: unset

Set DOCTOR_LLM_PROVIDER=openai and point at any OpenAI-compatible endpoint. The base URL can only be set here, never from the Settings page, since the API key is sent to it. Verify it in one command before you rely on it: docker compose exec container-doctor python -m app.selftest Free tiers, limits and prices below are as of October 2026 - check the vendor’s page, these change often. Google Gemini - free tier, no card. See https://sebastianbuechler.github.io/container-doctor/guides/models/ (“Free options, ranked”) for the privacy caveat: free-tier inputs may be used for training.

Default: anthropic · One of: anthropic, openai · Applies on restart

Built-in prices (USD per million tokens, in / out): claude-opus-5-5 4/20, claude-sonnet-5-5 2/10, claude-haiku-5-5 0.10/0.50, claude-fable-5 10/50.

Default: claude-opus-5-5

OpenAI-compatible endpoint (Ollama, LM Studio, OpenRouter, vLLM, …). Environment-only, like every URL a secret is sent to: a setting saved from the page could otherwise send the key to a host of its choosing.

Default: unset · Environment only

Anthropic API key. Without it the doctor still detects incidents and shows them in the dashboard - it just cannot explain them.

Default: unset · Secret

OpenAI-compatible endpoint (Ollama, LM Studio, OpenRouter, vLLM, …). Environment-only, like every URL a secret is sent to: a setting saved from the page could otherwise send the key to a host of its choosing.

Default: unset · Secret

Low | medium | high | xhigh | max - how hard the model thinks per incident. Only applies to Anthropic models; ignored elsewhere.

Default: medium · One of: low, medium, high, xhigh, max

Reasoning models reject temperature (GPT-6 Luna does at any reasoning effort above none). The request drops it automatically if the vendor complains; set -1 to never send it and skip the extra round trip.

Default: 0.2 · Type: number

USD per million tokens, so the spend meter works for a non-Anthropic vendor. Leave at -1 to price known Anthropic models from the built-in table and treat everything else as free. Set both to 0 to say “this really is free”.

Default: -1 · Type: number

USD per million tokens. Unset prices Anthropic models from a built-in table and counts anything else as free - hence the call cap above.

Default: -1 · Type: number

No new calls today once recorded spend reaches this (calls in flight can overshoot slightly; set a limit with the provider for an absolute ceiling). Manual re-diagnoses count too. 0 disables the cap.

Default: 1 · Type: number

No new calls after this many today. This is the cap that matters on a free tier, where the dollar meter reads zero all day. 0 disables it.

Default: 0 · Type: whole number

Identical failures reuse a stored diagnosis for this many hours (no API call).

Default: 6 · Type: whole number

Every request as it was sent, on the LLM page, for this many days. 0 keeps none; the daily spend totals are kept either way.

Default: 14 · Type: whole number

Share of the WHOLE host’s CPU, not of a single core.

Default: 85 · Type: number

Share of the container’s memory limit (or of host RAM when no limit is set).

Default: 90 · Type: number

Consecutive polls a breach must persist for, so one backup spike is quiet.

Default: 4 · Type: whole number

Consecutive polls a breach must persist for, so one backup spike is quiet.

Default: 3 · Type: whole number

Consecutive polls a breach must persist for, so one backup spike is quiet.

Default: 15 · Type: whole number

Consecutive polls a breach must persist for, so one backup spike is quiet.

Default: 120 · Type: whole number

Log lines older than this are never read; 0 reads the whole tail.

Default: 60 · Type: whole number

Lines matching DOCTOR_LOG_ERROR_PATTERN by wording.

Default: 5 · Type: whole number

Lines the app itself marked ERROR. Higher, because one ERROR line is weaker evidence than “no space left on device”.

Default: 10 · Type: whole number

A single FATAL / CRITICAL line opens an incident on its own.

Default: 1 · Type: whole number

Redeploys. A stop that Docker announced with a signal (docker stop, Compose, a Docker manager) is not a crash, and a container that exits and is replaced within DOCTOR_DIE_GRACE_SECONDS was redeployed. For DOCTOR_DEPLOY_SETTLE_MINUTES after any redeploy, errors other containers log while it restarts do not count, and a failing health check waits. 0 turns the window off.

Default: 3 · Type: number

A nonzero exit nobody asked for waits this long before it is a crash: if the container was replaced meanwhile, it was a redeploy.

Default: 30 · Type: number

Log lines to never count, host-wide. Prefer the per-container label doctor.ignore_log - noise is usually one container’s habit.

Default: unset

Free space is a property of the filesystem, so /data reports all of the volume it lives on without mounting it. To watch another volume, mount an empty directory on it read-only (never the volume root) and add it here: one path per volume, each with its own forecast.

Default: none · Type: comma-separated list

A watched filesystem this full (%) is a warning…

Default: 95 · Type: number

…and this full, critical.

Default: 98 · Type: number

The fill-rate forecast: an incident when a filesystem is on course to be full within DOCTOR_DISK_FORECAST_DAYS, critical within the _CRITICAL_DAYS. The rate is fitted over DOCTOR_DISK_TREND_WINDOW_DAYS of samples, one per DOCTOR_DISK_SAMPLE_MINUTES.

Default: 21 · Type: whole number

How far back the fill-rate is fitted, and how close to full is worth saying something about. A percentage says how full; these say whether it is going anywhere.

Default: 5 · Type: whole number

How far back the fill-rate is fitted, and how close to full is worth saying something about. A percentage says how full; these say whether it is going anywhere.

Default: 14 · Type: whole number

Software RAID (md) state; off turns the RAID check off.

Default: /proc/mdstat · off turns it off

Array layout, scrub results and per-disk read errors; off skips them.

Default: /sys · Applies on restart · off turns it off

New corrected read errors on one drive within a day that make its warning critical (0 never escalates), and how many days a drive warns after its last new one.

Default: 10 · Type: whole number

How long a drive’s newly corrected read errors keep its warning up. md’s own count only goes back to 0 on a reboot, so without a window a drive that had a bad day would warn until the next one.

Default: 7 · Type: whole number

The host itself (Host page). Read from /proc, which is system-wide; off turns it off. An incident opens when a condition lasts DOCTOR_HOST_BREACH_POLLS.

Default: /proc · Applies on restart · off turns it off

Below this share of memory available (the kernel’s MemAvailable, which counts reclaimable cache) the host is about to swap or OOM-kill.

Default: 5 · Type: number

Pages read back from swap per second. Swap being used is normal - Linux, DSM especially, parks idle pages there - swap being read back steadily is not.

Default: 256 · Type: number

The 5-minute load average per CPU. Linux load counts processes waiting on disks as well as on CPU, so it reads high during a scrub.

Default: 2 · Type: number

Share of CPU time spent waiting on disks.

Default: 40 · Type: number

Pages read back from swap per second. Swap being used is normal - Linux, DSM especially, parks idle pages there - swap being read back steadily is not.

Default: 80 · Type: number

Consecutive polls a host breach must last: ten is five minutes at the default interval, so a backup’s burst of I/O wait stays quiet.

Default: 10 · Type: whole number

One stored point per this many minutes, for the Host page’s charts and each drive’s temperature and sleep.

Default: 5 · Type: whole number

Internet and DNS (Network page), checked from the doctor’s container.

Default: 60 · Type: whole number · Applies on restart

Addresses, not names: reaching them says the line works, whatever DNS does.

Default: 1.1.1.1:443, 9.9.9.9:443 · Type: comma-separated list

Resolved the way containers resolve names.

Default: cloudflare.com, github.com · Type: comma-separated list

Your own DNS servers, each checked on its own: ip, ip:port or a container name.

Default: none · Type: comma-separated list

DNS servers asked directly - a Pi-hole, AdGuard, the router - as ip, ip:port or a container name on a network the doctor shares. Empty by default: which ones exist is the network’s business.

Default: https://www.cloudflare.com/cdn-cgi/trace · Type: comma-separated list

Rounds in a row a check must fail before an incident opens, and pass before it closes.

Default: 3 · Type: whole number

Internet and DNS, checked from the doctor’s container every this many seconds. 0 turns the checks off. “none” empties any of the lists.

Default: 5 · Type: number

The SMART sidecar’s metrics page. off turns drive health off.

Default: http://doctor-smart:9633/metrics · off turns it off

The SMART sidecar’s metrics page. off turns drive health off.

Default: 55 · Type: number

The SMART sidecar’s metrics page. off turns drive health off.

Default: 70 · Type: number

Share of an SSD’s rated write endurance used before it is worth a word.

Default: 90 · Type: number

Cold drives: read only every DOCTOR_SMART_COLD_HOURS (default once a day) instead of every five minutes, by a second reader in the sidecar. For drives that even the question “are you asleep?” wakes - an expansion unit in deep sleep powers up for it. List their device files, comma separated; the everyday reader then leaves them alone. Their sleep is not tracked.

Default: none · Type: comma-separated list

Cold drives: read only every DOCTOR_SMART_COLD_HOURS, by a second reader in the sidecar, because even asking whether they sleep wakes them - an expansion unit in deep sleep powers up for it. Their device files, comma separated, as for DOCTOR_SMART_DEVICES; the sidecar reads the same list.

Default: 24 · Type: number

Cold drives: read only every DOCTOR_SMART_COLD_HOURS, by a second reader in the sidecar, because even asking whether they sleep wakes them - an expansion unit in deep sleep powers up for it. Their device files, comma separated, as for DOCTOR_SMART_DEVICES; the sidecar reads the same list.

Default: http://doctor-smart:9634/metrics

Auto-applies: only in auto mode.

Default: restart_container · Any of: none, notify_only, restart_container, stop_container, prune_images

  • observe - diagnose and notify, never touch anything <- start here
  • suggest - propose an action, wait for you to approve it in the dashboard
  • auto - apply allowed actions without asking

Default: 0.75 · Type: number

Circuit breakers: at most this many actions on one container, and on the whole host, in any hour. doctor.max_actions_per_hour tightens the first.

Default: 3 · Type: whole number

Circuit breakers: at most this many actions on one container, and on the whole host, in any hour. doctor.max_actions_per_hour tightens the first.

Default: 10 · Type: whole number

Blocks re-running the same action for the same fingerprint.

Default: 30 · Type: whole number

Seconds to wait after an action before checking whether it actually helped.

Default: 45 · Type: whole number

A recurrence within this many hours of the last sighting folds into the open incident instead of opening a new one.

Default: 24 · Type: whole number

An incident whose condition has not re-fired for this long resolves itself; 0 turns that off.

Default: 12 · Type: whole number

Read GitHub release notes for images with a newer version, to link them and flag the ones that mention breaking changes. One unauthenticated request per such image per check.

Default: 30 · Type: whole number

OIDC sign-in through your own provider (Pocket ID, Authentik, Keycloak…). Empty issuer turns it off. The provider’s callback URL is <DOCTOR_PUBLIC_URL>/auth/callback.

Default: unset · Environment only

The client ID registered with the provider for the doctor.

Default: unset · Environment only

The dashboard’s own address, as the browser reaches it. Also used for the links in notifications.

Default: unset · Environment only

Protects the dashboard. Required; a long random one, e.g. openssl rand -base64 24. With neither a password nor OIDC set, the dashboard and its API refuse every request, because the dashboard can restart containers. Known placeholders (change-me, changeme, password, admin, doctor, secret, 123456, the user name…) are refused: the doctor logs a warning and treats sign-in as unconfigured. Under 12 characters works, with a warning at startup.

Default: unset · Secret

The client ID registered with the provider for the doctor.

Default: unset · Secret

Comma-separated groups allowed in; empty lets in any account the provider authenticates.

Default: none · Type: comma-separated list · Environment only

Hours an unused session lasts.

Default: 12 · Type: whole number · Environment only

Signs the session cookie. Unset, one is generated once and kept in the data directory (session_secret, mode 0600). openssl rand -base64 32

Default: unset · Secret

With OIDC on, the password is refused unless this is true - a way back in when the provider itself is what is broken.

Default: off · Type: on / off · Environment only

Read-only token for a Homepage tile: opens /api/widget, a few counts, and nothing else. openssl rand -hex 24

Default: unset · Secret

The summary at the top of the dashboard. false leaves the facts without a model’s words. An empty model uses DOCTOR_MODEL; a smaller one is plenty.

Default: on · Type: on / off

A smaller model is plenty for three lines of summary. Empty: DOCTOR_MODEL.

Default: unset

How often a section may be rewritten when its facts change without its level changing. A new critical incident does not wait for this.

Default: 15 · Type: whole number

A decision model’s second opinion on judgement signals (error bursts, health checks, CPU/memory) before they become incidents. off | shadow | on. shadow records verdicts and changes nothing - run it that way first.

Default: off · One of: off, shadow, on

Openrouter: a decision model via OpenRouter’s Decisions API (Jev). chat: the diagnosis’s own model; uncalibrated, so it never hides anything.

Default: openrouter · One of: openrouter, chat

Pinned rather than -latest, so verdicts do not drift under you.

Default: typesafe/jev-1.13

The Decisions API endpoint the key is sent to.

Default: https://openrouter.ai/api/alpha/decisions · Environment only

An OpenRouter key. Give it its own credit limit on openrouter.ai. Unset: the diagnosis’s DOCTOR_OPENAI_API_KEY, if that is an OpenRouter one.

Default: unset · Secret

Likely-noise probability at or above which a signal opens no incident (on).

Default: 0.9 · Type: number

Its own caps, separate from the diagnosis’s.

Default: 500 · Type: whole number

Probability of noise at or above which a signal opens no incident.

Default: 0.1 · Type: number

A host coming back up fires everything at once; past this, the rules decide alone until the minute is over.

Default: 10 · Type: whole number

A host coming back up fires everything at once; past this, the rules decide alone until the minute is over.

Default: 6 · Type: whole number

A host coming back up fires everything at once; past this, the rules decide alone until the minute is over.

Default: 5 · Type: number

How often to ask the registries what they publish; 0 turns the check off.

Default: 12 · Type: whole number · Applies on restart

Days a release must be on Docker Hub before it counts as newer - match your Dependabot or Renovate cooldown, if you use one. 0: at once.

Default: 3 · Type: whole number

Read GitHub release notes for images with a newer version.

Default: on · Type: on / off

Redeploy a stack from the Updates page when its image has a new build. The doctor runs a GitHub Actions workflow in the repository the stacks are deployed from, which redeploys the stack - see examples/redeploy-workflow.yml for what it must accept. A fine-grained token on that repository only, with Actions: read and write, and Contents: read. Unset, the page only reports.

Default: unset · Secret

Owner/repository the stacks are deployed from; see examples/redeploy-workflow.yml.

Default: unset

Owner/repository the stacks are deployed from; see examples/redeploy-workflow.yml.

Default: deploy-stacks.yml

Owner/repository the stacks are deployed from; see examples/redeploy-workflow.yml.

Default: main

Ntfy push notifications, off until both a server URL and a topic are set. The URL and token are environment-only. On ntfy.sh anyone who knows or guesses a topic’s name can read it, so pick a long random one (openssl rand -hex 16), protect it with an access token, or run your own ntfy server.

Default: unset · Environment only

The ntfy topic notifications are published to. No default: a topic on a public server is readable by anyone who guesses its name, so it has to be chosen - long and random on ntfy.sh.

Default: unset

Access token for a protected ntfy topic.

Default: unset · Secret

Stamped into the image by CI. “dev” means a local build.

Default: dev · Environment only

Evidence sent per call, in characters (about 3,000 tokens).

Default: 12000 · Type: whole number

Input price, USD per million tokens, when OpenRouter does not report the cost itself. Jev’s as of October 2026 - check the model page.

Default: 0.042 · Type: number

Where the database lives inside the container; mount a volume here.

Default: /data · Environment only

How often a disk’s free space is recorded for the fill forecast.

Default: 60 · Type: whole number

Comma-separated container names the doctor must never look at or touch.

Default: none · Type: comma-separated list

Retries on 429 / 5xx, honouring Retry-After. Free tiers rate-limit hard.

Default: 3 · Type: whole number

Free tiers rate-limit hard (Gemini free: 10/minute); retry rather than lose the diagnosis.

Default: 180 · Type: number

The wording that counts as an error, as a Python regular expression. The default matches fatal, panic, traceback, segfault, out of memory, oomkill, connection refused, no such host, permission denied, disk full, no space left, deadlock and unhandled exception, case-insensitively.

Default: (?i)\b(fatal|panic|traceback|segfault|out of memory|oomkill|connection refused|no such host|permission denied|disk full|no space left|deadlock|unhandled exception)\b

The release, X.Y.Z, for an image the Release workflow built; empty otherwise.

Default: INFO · Environment only

Upper bound on the length of one answer.

Default: 4000 · Type: whole number

The ID token claim holding the user’s groups, for DOCTOR_OIDC_ALLOWED_GROUPS.

Default: groups · Environment only

Scopes requested at sign-in, space-separated.

Default: openid profile email groups · Environment only

The doctor’s own container name, which it never diagnoses or restarts.

Default: container-doctor

Settings can be changed on the Settings page by anyone who can sign in; a variable set here always overrides what was saved there. false makes the page read-only.

Default: on · Type: on / off · Environment only

The release, X.Y.Z, for an image the Release workflow built; empty otherwise.

Default: unset · Environment only

Address and port the dashboard listens on inside the container.

Default: 0.0.0.0 · Environment only

Address and port the dashboard listens on inside the container.

Default: 8080 · Type: whole number · Environment only

The user name that password signs in as.

Default: doctor · Environment only

These never reach the doctor: the reference compose file reads them itself.

Set to 0 to close the socket proxy’s only write route. The doctor then physically cannot restart anything, whatever mode it is in. (This maps to ALLOW_RESTARTS on the proxy; the global POST gate is always off, which is what keeps /containers/create - arbitrary host mounts - out of reach.)

0 denies the read-only GET /images//json on the proxy; the image update check then says it cannot run.

Where the doctor reaches Docker. Point it at a socket proxy you already run (see https://sebastianbuechler.github.io/container-doctor/guides/socket-proxy/, “Reusing yours instead”).

Drive health from the doctor-smart sidecar (Storage page). The sidecar is opt-in, since it has raw disk access: start the stack with docker compose --profile smart up -d, or set COMPOSE_PROFILES=smart. “auto” finds the drives; or list their device files, space or comma separated. Give SATA drives their type, /dev/sata1;sat, or smartctl may read a WD as SCSI.

Drive health from the doctor-smart sidecar (Storage page). The sidecar is opt-in, since it has raw disk access: start the stack with docker compose --profile smart up -d, or set COMPOSE_PROFILES=smart. “auto” finds the drives; or list their device files, space or comma separated. Give SATA drives their type, /dev/sata1;sat, or smartctl may read a WD as SCSI.

Read by compose.example.yaml itself: the dashboard’s port on the host, and where the database lives on the host and who owns it. Create the directory and chown it to DOCTOR_UID:DOCTOR_GID before the first start.

Read by compose.example.yaml itself: the dashboard’s port on the host, and where the database lives on the host and who owns it. Create the directory and chown it to DOCTOR_UID:DOCTOR_GID before the first start.

Read by compose.example.yaml itself: the dashboard’s port on the host, and where the database lives on the host and who owns it. Create the directory and chown it to DOCTOR_UID:DOCTOR_GID before the first start.

Read by compose.example.yaml itself: the dashboard’s port on the host, and where the database lives on the host and who owns it. Create the directory and chown it to DOCTOR_UID:DOCTOR_GID before the first start.

Pin to a specific published image instead of tracking latest, e.g. X.Y.Z or a sha- tag from ghcr.io, to roll back or freeze a deploy.

Pin to a specific published image instead of tracking latest, e.g. X.Y.Z or a sha- tag from ghcr.io, to roll back or freeze a deploy.