Every setting is an environment variable read from the .env file in the installation directory
(the Compose env_file); start from the installation example. Variables
marked required stop the bot at startup when missing; malformed values (unknown AI_PROVIDER,
a web search provider without its URL or key, bad Slack user IDs, malformed URLs) are always fatal,
one line per problem.
Which features exist on which backend is in BACKENDS.md.
| Variable | Default | Description |
|---|---|---|
SLACK_BOT_TOKEN |
required | Bot User OAuth Token (xoxb-...) from the installed app |
SLACK_APP_TOKEN |
required | App-level token (xapp-...) with connections:write for Socket Mode |
SLACK_API_URL |
https://slack.com/api |
Alternate Slack API base (GovSlack, proxies) |
The app is created from manifest.json (Agent messaging experience, Socket Mode,
no inbound ports). The team:read bot scope lets the bot read the workspace name for its system
prompt; it is optional: without it the bot falls back to the team name from auth.test and logs a
warning.
Switching an existing Slack app from
assistant_viewtoagent_viewis irreversible. Update and test the application code before applyingmanifest.jsonto an existing installation. Users need to hard-refresh Slack after the manifest update. Slack CLI 4.4.0 or newer is required to apply an Agent View manifest.
| Variable | Default | Description |
|---|---|---|
AI_PROVIDER |
openai |
openai, ollama, vllm or openai-compatible (see BACKENDS.md) |
M8B_START_DEGRADED |
false |
Start even when the AI backend health check fails (the bot then answers every message with an error until the backend is back) |
AI_MAX_AGENT_ITERATIONS |
15 |
Cap on model → tools → model loops per Slack message. The last permitted iteration forces a text answer |
At startup the bot logs the active backend and runs a health check: reachability, model presence and
one minimal streaming /v1/responses request, so a backend that lists models but cannot answer is
caught before the bot goes online. Function-tool support is not probed at startup; the
doctor script does that. A failed check is fatal (exit code 1) unless
M8B_START_DEGRADED=true.
AI provider: ollama
AI model: qwen3.8:27b
AI endpoint: http://ollama-host:11434/v1
AI backend health check passed (model "qwen3.8:27b" available)
AI_PROVIDER=openai)| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
required | API key with access to gpt-5.6-sol and the Responses API hosted tools |
OPENAI_VECTOR_STORE_IDS |
unset | Comma-separated vector store IDs for the hosted knowledge base |
OPENAI_VECTOR_STORE_ID |
unset | Single-store alternative to the above |
OPENAI_CODE_CONTAINER_ID |
unset | Reuse a Code Interpreter container (cc_...) instead of an automatic one |
The model configuration lives in ai/config/system-prompt.js: gpt-5.6-sol, reasoning effort
medium with automatic summaries, low verbosity, up to 8,000 output tokens, previous_response_id
continuity, and a privacy-preserving safety_identifier hashed from the workspace and user IDs.
When server-side history grows past about 100k tokens (or a context-window error is hit), older
messages are summarized with gpt-4o-mini. MetricsHub tools are grouped into namespaces of fewer
than ten functions; ListHosts and SearchHost stay immediately callable, larger schemas, PromQL
and knowledge-base writes are loaded through hosted tool search on demand. Large MCP outputs are
uploaded as files so Code Interpreter can analyze the complete result.
ollama, vllm and openai-compatible read the same AI_* variables. AI_PROVIDER is a
preset: it fixes the defaults and the backend quirks (Ollama’s sidecar vision, vLLM’s strict chat
template and single-model adoption). OPENAI_API_KEY is not required and nothing is ever sent to
OpenAI.
| Variable | Default | Description |
|---|---|---|
AI_BASE_URL |
Ollama http://localhost:11434/v1, vLLM http://localhost:8000/v1, required for openai-compatible |
Endpoint serving /v1/responses. Inside a container, localhost is the bot container |
AI_API_KEY |
ollama / vllm / none |
Bearer token; the SDK requires one even when the server ignores it |
AI_MODEL |
Ollama qwen3.8:27b; others: the single served model |
Chat model. With several served models (a gateway) it is required; the bot never guesses and refuses to start without one |
AI_CONTEXT_LENGTH |
detected, else 32768 with a warning |
The model’s context window in tokens as the server allocates it (see below) |
AI_MAX_OUTPUT_TOKENS |
4000 |
Output cap per turn. In vLLM and openai-compatible modes, a value leaving the prompt fewer than 8k tokens is flagged at startup |
AI_REQUEST_TIMEOUT_MS |
300000 |
Per-request timeout |
AI_MAX_TOOL_OUTPUT_CHARS |
~40% of the usable budget | Inline cap (chars) for one tool result before truncation/staging |
Context length detection. In Ollama mode the bot reads the server’s effective context from the
native API (/api/ps for a loaded model, /api/show for a Modelfile num_ctx); in vLLM and
openai-compatible modes it reads max_model_len / context_length from /v1/models when reported.
An unset AI_CONTEXT_LENGTH adopts the detected value; a smaller configured value is kept (tighter
budgets are safe); a larger one is capped to the server’s value with a warning. The bot can never
believe it has more context than the server allocates. Most gateways report nothing: set it
explicitly there.
Deprecated aliases. The former OLLAMA_* / VLLM_* names of these settings (OLLAMA_MODEL,
VLLM_BASE_URL, …) still work for their preset and take precedence over AI_*; the bot lists
them once at startup with their replacements. OLLAMA_CONTEXT_LENGTH is a permanent alias of
AI_CONTEXT_LENGTH, named after Ollama’s own server variable so one line configures both sides.
OLLAMA_CONTEXT_WINDOW is no longer read.
AI_PROVIDER=ollama)| Variable | Default | Description |
|---|---|---|
OLLAMA_CONTEXT_LENGTH |
unset | Permanent alias of AI_CONTEXT_LENGTH; must match Ollama’s num_ctx |
OLLAMA_VISION_MODEL |
unset | Sidecar vision model that describes image attachments as text (e.g. qwen3-vl:8b-instruct-8k). Unset = images surfaced as “cannot analyze” |
OLLAMA_VISION_MAX_OUTPUT_TOKENS |
600 |
Cap on the description; the vision model’s own (often 8k) context must hold image + prompt + output |
AI_PROVIDER=ollama
AI_BASE_URL=http://ollama-host:11434/v1
AI_MODEL=qwen3.8:27b
AI_EMBEDDING_MODEL=nomic-embed-text
OLLAMA_CONTEXT_LENGTH=32768
OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct-8k
On the Ollama host: ollama pull qwen3.8:27b, ollama pull nomic-embed-text and optionally
ollama pull qwen3-vl:8b-instruct-8k.
The main chat model is text-only and Ollama’s /v1/responses has no image input, so screenshots
are handled by the vision model through /v1/chat/completions: the bot downloads the image from
Slack, asks for a factual description (verbatim text, chart trends, anomalies) with a short snippet
of the conversation for focus, and injects that description as text. Descriptions are stored in the
per-thread conversation, so each image is described once.
AI_PROVIDER=vllm)No vLLM-specific variables. The preset turns on native image input and strict input conforming,
adopts the single served model when AI_MODEL is unset, and requires /v1/models at the health
check. Embeddings need a dedicated endpoint (a vLLM instance serves one model), see the knowledge
base section. Screenshots go through the media store, see the images section.
AI_PROVIDER=vllm
AI_BASE_URL=http://vllm-host:8000/v1
AI_API_KEY=vllm_... # whatever the reverse proxy in front of vLLM expects
# AI_MODEL= # adopted automatically
# AI_CONTEXT_LENGTH= # detected from max_model_len
M8B_MEDIA_BASE_URL=https://bot-host.example.com/m8b-media
# AI_EMBEDDING_BASE_URL=http://vllm-host:8001/v1
# AI_EMBEDDING_MODEL=qwen3-embedding-0.6b
AI_PROVIDER=openai-compatible)| Variable | Default | Description |
|---|---|---|
AI_IMAGE_INPUT |
false |
The model reads images natively (input_image items). Then configure the media store as in vLLM mode |
AI_STRICT_INPUT |
false |
One leading system message only, assistant history as plain strings. Enable on 400 System message must be at the beginning |
AI_PROVIDER=openai-compatible
AI_BASE_URL=https://inference.example.com/v1
AI_API_KEY=...
AI_MODEL=llama-3.3-70b-instruct # required when the gateway serves several models
AI_CONTEXT_LENGTH=131072 # gateways rarely report it
# AI_EMBEDDING_MODEL=nvidia/nv-embedqa-e5-v5
The endpoint must implement /v1/responses with streaming and function tools;
/v1/chat/completions alone is not enough. The health check calls GET /v1/models: AI_MODEL
must be listed there when the endpoint exposes it. A gateway without /v1/models passes with a
warning (model unverified); authentication or server errors on it fail the check. Only universally
supported request fields are sent (model, input, tools, stream, max_output_tokens);
OpenAI-only fields never are. vLLM works through this preset too: it is openai-compatible +
AI_IMAGE_INPUT=true + AI_STRICT_INPUT=true.
The prompt (ai/config/system-prompt.js) is deployment-neutral and ships with the bot: persona,
safety rules (no fabrication, action boundaries, credential handling) and tool guidance are the
same for every installation and are not configurable. Two things adapt it to your organization.
| Variable | Default | Description |
|---|---|---|
M8B_PROMPT_EXTRA_FILE |
unset | Markdown/text file appended as a delimited “Deployment notes” section. Relative paths resolve against the working directory; an unreadable file stops the bot |
M8B_PROMPT_EXTRA |
unset | Inline notes appended after the file’s content |
team.info with the
team:read scope, else the auth.test team name) and injected where the prompt refers to the
company. On an Enterprise Grid org-wide install each workspace is resolved on its first message.Changes take effect on restart, including for open threads: in OpenAI mode each reply carries a fingerprint of the prompt it ran under and a thread started under a different prompt is re-seeded with the current one; self-hosted modes always resend the prompt. The bot logs the resolved organization and the size of the appended notes.
OpenAI mode uses hosted file_search on the vector stores listed in OPENAI_VECTOR_STORE_IDS.
Self-hosted modes use a local RAG store: Markdown documents in M8B_DATA_DIR/knowledge/docs/ are
chunked, embedded through /v1/embeddings and persisted in knowledge/index.json; retrieval is
cosine similarity, exposed as search_knowledge_base, and update_knowledge writes new entries
into the same store. This flat-file design is intentional for a small corpus.
| Variable | Default | Description |
|---|---|---|
AI_EMBEDDING_MODEL |
Ollama nomic-embed-text; others unset |
Embedding model. Unset = knowledge base disabled (neither search nor writes are offered to the model) |
AI_EMBEDDING_BASE_URL |
AI_BASE_URL |
Embedding endpoint. Required in vLLM mode (a vLLM instance serves one model): a second instance or an Ollama server |
AI_EMBEDDING_API_KEY |
AI_API_KEY |
Bearer token for the embedding endpoint |
AI_EMBEDDING_QUERY_PREFIX |
per model | Task prefix for searches (nomic: search_query:; mxbai: query instruction). Set both prefixes empty to send none |
AI_EMBEDDING_DOCUMENT_PREFIX |
per model | Task prefix for indexing (nomic: search_document:) |
AI_EMBEDDING_QUERY_INPUT_TYPE |
query for NVIDIA nv-embed*/embedqa |
Per-request input_type some APIs require |
AI_EMBEDDING_DOCUMENT_INPUT_TYPE |
passage for NVIDIA models |
Same, when indexing |
KNOWLEDGE_BASE_DIR |
M8B_DATA_DIR/knowledge |
Override of the store location |
In vLLM and openai-compatible modes the embedding endpoint is probed once at startup (warning
only); in Ollama mode a missing embedding model is only noticed on the first search, so pull it
before starting (the doctor script checks it in every mode). A failing embedding
call at runtime makes search_knowledge_base report the knowledge base as unavailable for that
call; nothing else breaks.
Migrating an OpenAI vector store (the source documents live only in OpenAI): export, switch
.env to the self-hosted backend (the indexer uses the configured provider and exits in OpenAI
mode), then index:
docker compose run --rm --no-deps -e OPENAI_API_KEY=sk-... -e OPENAI_VECTOR_STORE_IDS=vs_... \
m8b node scripts/export-openai-knowledge.js
# after switching .env to the self-hosted backend:
docker compose run --rm --no-deps m8b node scripts/index-knowledge.js
Indexing is incremental where it can be: bot-written knowledge is embedded at write time;
node scripts/index-knowledge.js /var/lib/m8b/knowledge/docs/<file>.md [...] re-embeds only the
listed documents; a full run is required after adding or removing many documents, or whenever you
switch embedding models, prefixes or input types (the index refuses mixed-configuration writes and
searches with a clear “rebuild” error). The index is written through a temporary file, so an
interrupted run keeps the previous index. The running bot caches the index in memory: recreate
the container after a rebuild.
Run docker compose run --rm --no-deps m8b node scripts/index-knowledge.js then
docker compose up -d --force-recreate m8b (see Docker Compose): restart keeps
the old container’s environment, so a rebuild that follows an embedding change would leave the bot on
the previous model and every search rejected as a configuration mismatch.
A full index-knowledge.js run (local providers only — OpenAI mode uses hosted file_search and
this script does not populate vector stores) first downloads the official documentation, then
rebuilds the combined index, so a fresh installation answers installation, configuration,
connector and troubleshooting questions with citations without any manually supplied document:
https://metricshub.com/docs/…
targets (redirects included) are accepted.knowledge/docs/metricshub-official-<url-hash>.md with their front matter
intact; ownership and provenance (URL, title, retrieval date, content hash) live in
knowledge/official-docs.json. Only files listed there are ever pruned or overwritten, so
user-authored, exported and bot-written documents are preserved across refreshes.index-knowledge.js --skip-docs-download. File-specific indexing never downloads.
Manifest entries the site answers 404 for are retried once as <name>/index.md (the manifest
lists section pages that way), then reported as missing in the output and in
official-docs.json rather than indexed; more than 10% missing pages, or more than 10% of
manifest links outside the documentation scope, fails the refresh and keeps the previous
snapshot.source plus the retrieval date, so
the bot can cite them. The corpus is the latest release: answers may not match an older
installed MetricsHub version. update_knowledge cannot rewrite these pages; corrections and
local procedures are written as separate articles.qwen3-embedding-0.6b (1024 dimensions), index.json about 58 MB, first search about 3 s
(index load) then about 200 ms. Budget longer on CPU. Bot-written knowledge is embedded at
write time and does not need this command.Retrieval tips: for a bilingual corpus bge-m3 usually beats nomic-embed-text; curate the
corpus, since cosine similarity has no notion of “deprecated” (delete legacy documents or mark them
with an explicit first line such as **Status: DEPRECATED — replaced by X**).
Applies to vLLM mode and to openai-compatible with AI_IMAGE_INPUT=true. Screenshots go to the
model natively as input_image items. Because the local conversation history is resent on every
turn, images should not be embedded as base64 in it (without M8B_MEDIA_BASE_URL they are, and
every later turn resends them). With a configured media store the bot downloads the image from Slack
once (with its own Slack credentials, never given to the inference host), saves it under
M8B_MEDIA_DIR as <uuid>.<ext>, and stores only the URL M8B_MEDIA_BASE_URL/<uuid>.<ext> in the
conversation. A reverse proxy you run serves that directory to the inference host.
| Variable | Default | Description |
|---|---|---|
M8B_MEDIA_BASE_URL |
unset | Public base URL of the media directory as seen from the inference host. Unset = base64 fallback (dev only, startup warning) |
M8B_MEDIA_DIR |
M8B_DATA_DIR/media |
Where images are stored |
M8B_MEDIA_RETENTION_DAYS |
7 |
Age-based cleanup, swept at startup and every 6 hours |
M8B_MEDIA_MAX_FILE_BYTES |
10485760 |
Oversized images degrade to an explicit note |
Only PNG, JPEG, GIF and WebP are passed through. If a stored conversation still references a deleted image, the reference is replaced by a text marker before the request is sent; threads rebuilt from Slack history re-download their images. The bot itself does not serve the media directory over HTTP:
# NGINX on the bot host: expose the media dir to the inference host ONLY
location /m8b-media/ {
alias /var/lib/m8b/media/;
allow <inference-host-IP>;
deny all;
autoindex off;
}
# vLLM systemd unit: restrict remote media fetching to the bot host (anti-SSRF)
# and enable the bounded media-download cache
# vllm serve ... --allowed-media-domains bot-host.example.com
Environment=VLLM_MEDIA_URL_ALLOW_REDIRECTS=0
Environment=VLLM_MEDIA_CACHE=/var/lib/vllm/media-cache
Environment=VLLM_MEDIA_CACHE_MAX_SIZE_MB=4096
Environment=VLLM_MEDIA_CACHE_TTL_HOURS=24
The hosted Code Interpreter is replaced by a run_python function tool backed by
Pyodide (CPython in WebAssembly, in a worker thread). The WASM boundary is
the sandbox: no network, no host filesystem except /data, a per-execution mount holding the inputs
staged by the app (user attachments and large tool outputs as JSON). /outputs collects files
posted back to the thread. numpy, pandas, matplotlib and openpyxl load on demand, downloaded once by
the host and cached. Pyodide’s js interop module is stripped at startup and every execution is
bounded by a hard timeout enforced with worker.terminate().
| Variable | Default | Description |
|---|---|---|
CODE_SANDBOX_ENABLED |
true |
false removes the tool; the prompt then asks for file contents inline |
CODE_SANDBOX_TIMEOUT_MS |
60000 |
Wall-clock limit per execution (the first import of a package downloads it) |
CODE_SANDBOX_PACKAGE_CACHE_DIR |
M8B_DATA_DIR/sandbox/packages |
Downloaded Python packages |
CODE_SANDBOX_MAX_OUTPUT_FILE_BYTES |
20971520 |
Total size of the files one execution may deliver to Slack |
CODE_SANDBOX_MAX_INPUT_FILE_BYTES |
104857600 |
Cap per user attachment staged into /data (streamed to disk, not buffered) |
CODE_SANDBOX_STAGING_DIR |
M8B_DATA_DIR/sandbox/staging |
Attachment download cache + per-execution /data directories |
CODE_SANDBOX_STAGING_CACHE_MAX_BYTES |
2147483648 |
Cache size before least-recently-used files are evicted |
OpenAI mode uses the hosted web search, which also reads pages. Self-hosted modes get an app-side
web_search tool (titles and snippets) and a fetch_url tool that reads the URLs users paste and
search results in full: the site’s Markdown rendition first (Accept: text/markdown, sibling .md,
/llms.txt), GitHub issues and pull requests anonymously through the REST API (public repositories
only, no tokens), otherwise the HTML page reduced to Markdown with Turndown after dropping
navigation, scripts and asides. Only public http/https hosts are read: private, loopback,
link-local and cloud-metadata addresses are refused at DNS resolution, redirects included. Binary
responses (PDFs included) are refused; long pages are staged for run_python.
| Variable | Default | Description |
|---|---|---|
WEB_SEARCH_PROVIDER |
unset | searxng or ollama-cloud. Unset/empty = web search unavailable. Compose defaults it to searxng |
SEARXNG_URL |
unset | SearXNG instance with JSON responses enabled (Compose: http://searxng:8080) |
OLLAMA_CLOUD_API_KEY |
unset | Key for ollama-cloud. Search queries leave your network (ollama.com) |
FETCH_URL_ENABLED |
true |
false removes the page reader |
FETCH_URL_ALLOWED_HOSTS |
unset | Comma-separated host suffixes (example.com also matches docs.example.com). Empty = any public host |
FETCH_URL_BLOCKED_HOSTS |
unset | Same syntax; blocked wins |
FETCH_URL_TIMEOUT_MS |
20000 |
Whole-tool network timeout |
FETCH_URL_MAX_BYTES |
2097152 |
Per-response size cap after decompression |
M8B connects to each MetricsHub agent through that agent’s MCP endpoint (URL + API token from the agent administrator). These settings configure MetricsHub agents; they do not enable arbitrary third-party MCP integrations.
| Variable | Default | Description |
|---|---|---|
M8B_MCP_CONFIG |
unset | Path to a JSON array of servers (see below). Highest precedence |
MCP_SERVERS |
unset | The same JSON array inline (use literal ${VAR} placeholders, quote against shell/Compose expansion) |
MCP_AGENT_URL |
unset | Single-server mode: endpoint URL (https://<agent-host>:31888/sse, or /mcp on MetricsHub versions serving Streamable HTTP). Requires MCP_AGENT_TOKEN |
MCP_AGENT_TOKEN |
unset | Single-server mode: API token |
MCP_AGENT_LABEL |
hostname | Single-server mode: label the bot uses for the agent (defaults to the hostname of MCP_AGENT_URL) |
MCP_AGENT_TRANSPORT |
probed | http (Streamable HTTP, typically /mcp) or sse (legacy, typically /sse) |
MCP_ALLOW_SELF_SIGNED_CERT |
false |
Single-server mode: skip certificate verification (prefer trusted certificates) |
MCP_HOSTS_REFRESH_INTERVAL_MS |
3600000 |
Periodic refresh of the consolidated host map (ListHosts/SearchHost); also after config changes. 0 disables |
Precedence: M8B_MCP_CONFIG > MCP_SERVERS > legacy ai/mcp.config.local.js (deprecated) >
MCP_AGENT_URL/MCP_AGENT_TOKEN. The first configured source wins; empty variables count as unset;
[] disables MCP. Labels must be unique, URLs must be HTTP(S), every server needs a token, and
${ENV_VAR} references in token values must resolve to nonempty variables. Invalid configuration
stops startup with an entry-specific error rather than skipping servers.
[
{
"server_label": "lab-a",
"server_url": "https://metricshub-a.example.com/sse",
"token": "${MCP_LAB_A_TOKEN}",
"transport": "sse"
},
{
"server_label": "lab-b",
"server_url": "https://metricshub-b.example.com/mcp",
"token": "${MCP_LAB_B_TOKEN}",
"transport": "http",
"allowSelfSignedCert": false
}
]
When transport is omitted, M8B tries HTTP first, then SSE at the same URL if initialization
returns HTTP 400/404/405, an unsupported content type, or times out after 10 seconds; it remembers
the selected transport for reconnects. Authentication failures never trigger fallback. MCP and
MetricsHub REST requests reject redirects (so credentials are never forwarded) and ask for the final
URL. Server connection failures are logged but do not stop the bot; check the startup logs for
Connected to <label> using <transport>. Configuration is loaded at startup: restart after any
change. For a container, mount the JSON file and use its path inside the container.
The bot can edit MetricsHub configuration files and request credentials through the agent’s REST API (same origin and token as MCP), with Slack approval buttons and credential dialogs.
| Variable | Default | Description |
|---|---|---|
METRICSHUB_CONFIG_ADMINS |
unset | Comma-separated Slack user IDs allowed to request configuration changes. Empty = editing disabled |
METRICSHUB_INTERACTION_TIMEOUT_MS |
900000 |
How long a credential dialog or approval button stays valid |
The PromQL tool works with any backend that serves the Prometheus HTTP API (/api/v1/query and
/api/v1/query_range under the configured base URL): Prometheus, Mimir, Thanos, VictoriaMetrics,
Grafana Cloud Metrics, a Grafana data source proxy, Dash0, New Relic’s PromQL endpoint…
| Variable | Default | Description |
|---|---|---|
M8B_PROMETHEUS_URL |
unset | Base URL for the PromQL tool, path prefix included (Grafana Cloud: .../api/prom, Dash0: .../api/prometheus). Unset = no tool |
M8B_PROMETHEUS_BASIC_AUTH |
unset | user:password sent as HTTP Basic auth. Grafana Cloud: metrics instance ID and an access policy token with metrics:read |
M8B_PROMETHEUS_HEADERS |
unset | JSON object of extra headers, e.g. {"Authorization":"Bearer ..."} (Grafana data source, Dash0) or {"X-Query-Key":"..."} (New Relic) |
Datadog does not speak PromQL and is not covered by this tool.
| Variable | Default | Description |
|---|---|---|
M8B_DATA_DIR |
./data |
Root of all local stores, relative to the working directory. The Docker image sets /var/lib/m8b |
| Subdirectory | Override | Used by |
|---|---|---|
knowledge/ |
KNOWLEDGE_BASE_DIR |
Local knowledge base (with an embedding model) |
media/ |
M8B_MEDIA_DIR |
Native image input without provider uploads |
sandbox/packages/ |
CODE_SANDBOX_PACKAGE_CACHE_DIR |
Python sandbox |
sandbox/staging/ |
CODE_SANDBOX_STAGING_DIR |
Python sandbox |
conversations/ |
reserved | Future conversation persistence |
Startup creates and write-checks only the stores used by enabled features; OpenAI mode needs none.
An unwritable active store fails startup with its path. Ensure the bot user owns the directories;
if using the NGINX media proxy, point its alias at the resolved media directory. When upgrading
from a version whose sandbox caches lived under node_modules/.cache, stop the bot and move the data
(nothing is moved automatically) or keep the per-store overrides pointing at the old locations.
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
unset | production: warnings and errors. test: info and above. Anything else: debug (verbose) |
Info/debug logs include reasoning summaries, tool activity, token/cache usage and the complete LLM response. Treat development logs as potentially sensitive operational data.
The image is published at docker.metricshub.com and requires MetricsHub customer registry
credentials. Follow the installation steps to pull the image, create the Slack
app and .env.
The supplied docker-compose.yml and searxng/settings.yml are bundled in the image under
/app/deploy; the installation steps copy them into your installation directory. Keep the
searxng subdirectory next to the Compose file so its relative bind mount resolves correctly.
docker-compose.yml pulls docker.metricshub.com/metricshub/m8b-slack:latest (customer
credentials required; docker login docker.metricshub.com). Set M8B_IMAGE_TAG in .env to the
image tag you installed to pin a version. Upgrade with docker compose pull then
docker compose up -d; docker compose down preserves data, -v deletes it.
The container runs as the non-root node user (UID/GID 1000) with a named volume at
/var/lib/m8b; Compose fixes M8B_DATA_DIR to that mount. Back up this volume. Bind mounts must be
writable by UID/GID 1000. Use backend hostnames reachable from the container in AI_BASE_URL
and MCP URLs. Socket Mode needs no inbound ports. No container health check is declared until the
HTTP health endpoint (issue #13) exists. URL-based image delivery still needs the external media
proxy above.
For a JSON MCP config or a deployment notes file, add a read-only bind mount and set
M8B_MCP_CONFIG or M8B_PROMPT_EXTRA_FILE to its container path. Do not bake deployment files or
credentials into the image. In Compose .env files, single-quote values containing literal $
(including JSON with ${TOKEN_NAME} placeholders) to prevent Compose interpolation.
The maintenance scripts (doctor.js, index-knowledge.js, export-openai-knowledge.js) run in a
one-off container that reuses the image, the .env file and the data volume:
docker compose run --rm m8b node scripts/doctor.js
docker compose run --rm --no-deps m8b node scripts/index-knowledge.js
docker compose run --rm --no-deps -e OPENAI_API_KEY=sk-... -e OPENAI_VECTOR_STORE_IDS=vs_... \
m8b node scripts/export-openai-knowledge.js
Arguments follow the script path (node scripts/index-knowledge.js --skip-docs-download), -e
adds one-off variables, and --no-deps skips starting SearXNG, which only the doctor’s web search
probe needs. docker compose exec m8b ... works too when the bot is already running.
Compose starts a private SearXNG service (JSON enabled, no published port) and defaults
WEB_SEARCH_PROVIDER=searxng / SEARXNG_URL=http://searxng:8080; OpenAI mode keeps its hosted
search. searxng/settings.yml is a first-start template: SearXNG generates a secret and stores its
active settings in the searxng-config volume, so later template edits do not apply. To customize
an existing installation:
docker compose cp searxng:/etc/searxng/settings.yml ./searxng-active.yml
# edit, then
docker compose cp ./searxng-active.yml searxng:/etc/searxng/settings.yml
docker compose restart searxng
To use an external SearXNG, set SEARXNG_URL in .env (the instance must allow JSON responses),
start only the bot with docker compose up -d --no-deps m8b and stop the bundled service, or remove
the searxng service and the depends_on entry. WEB_SEARCH_PROVIDER= (empty) disables app-side
web search.
docker compose run --rm m8b node scripts/doctor.js
Checks the Slack tokens and granted scopes against manifest.json, connects to every MCP server
(tools and host counts, warns when a server reports no hosts), verifies the AI backend with a real
streaming /v1/responses call offering a function tool, probes the embeddings endpoint and the web
search backend when configured, write-tests the data directories and boots the Python sandbox. Exit
code 1 when any check fails; no secrets are printed.