chmonitor
Deployment

Docker

Run chmonitor as a Docker container or Compose service — the fastest path to a self-hosted ClickHouse dashboard.

Run chmonitor as a Docker container — the fastest path to a self-hosted ClickHouse dashboard. The image is published to GitHub Container Registry.

Prerequisites

  • Docker installed and running.
  • A reachable ClickHouse endpoint with a monitoring user (SELECT on system.*).
  • A release tag to pin.

Image and tags

Image: ghcr.io/chmonitor/chmonitor:vX.Y.Z — replace vX.Y.Z with a real release tag. Browse all releases.

The legacy name ghcr.io/chmonitor/chmonitor also works and points at the same build.

Setup

Pull and run

docker run -d --name chmonitor -p 3000:3000 \
  --add-host=host.docker.internal:host-gateway \
  -e CLICKHOUSE_HOST='http://host.docker.internal:8123' \
  -e CLICKHOUSE_USER='monitoring' \
  -e CLICKHOUSE_PASSWORD='change-me' \
  ghcr.io/chmonitor/chmonitor:vX.Y.Z

Open http://localhost:3000.

Linux host networking

--add-host=host.docker.internal:host-gateway is needed on Linux when ClickHouse runs on the Docker host. It is not needed on Docker Desktop for Mac/Windows.

services:
  chmonitor:
    image: ghcr.io/chmonitor/chmonitor:vX.Y.Z
    ports:
      - '3000:3000'
    environment:
      CLICKHOUSE_HOST: 'http://clickhouse:8123'
      CLICKHOUSE_USER: 'monitoring'
      CLICKHOUSE_PASSWORD: 'change-me'
    healthcheck:
      test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://localhost:3000/api/health']
      interval: 30s
      timeout: 5s
      start_period: 20s
      retries: 3

If ClickHouse runs in the same Compose project, use the service name as the host. If it runs on the Docker host machine, use host.docker.internal and add --add-host=host.docker.internal:host-gateway to the service's extra_hosts.

Verify

curl -sf http://localhost:3000/api/healthz && echo OK

Configure

All configuration is via environment variables passed as -e KEY=VALUE, a Compose environment: block, or an optional env_file: .env (the Compose file declares it with required: false). Use the canonical CHM_* names from apps/dashboard/.env.example — the same names work on every target. You can also mount a config file (see Feature permissions below).

ClickHouse connection

docker run -d --name chmonitor -p 3000:3000 \
  -e CLICKHOUSE_HOST='http://clickhouse:8123' \
  -e CLICKHOUSE_USER='monitoring' \
  -e CLICKHOUSE_PASSWORD='change-me' \
  -e CLICKHOUSE_NAME='prod' \
  ghcr.io/chmonitor/chmonitor:vX.Y.Z
VariableDefaultDescription
CLICKHOUSE_HOST— (required)ClickHouse URL(s), comma-separated for multi-host
CLICKHOUSE_USERdefaultUsername(s), same count as HOST
CLICKHOUSE_PASSWORD""Password(s), same count as HOST
CLICKHOUSE_NAMEFriendly label(s) shown in the host switcher

Multiple hosts

CLICKHOUSE_HOST defines the host count. CLICKHOUSE_USER and CLICKHOUSE_PASSWORD may be a single value (applied to all hosts) or one value per host position. CLICKHOUSE_NAME is optional. Position N maps to host N.

-e CLICKHOUSE_HOST='http://ch1:8123,http://ch2:8123' \
-e CLICKHOUSE_USER='monitoring,monitoring' \
-e CLICKHOUSE_PASSWORD='pass1,pass2' \
-e CLICKHOUSE_NAME='shard-1,shard-2'

Query / pool tuning

-e CLICKHOUSE_MAX_EXECUTION_TIME='30' \
-e CLICKHOUSE_TZ='UTC' \
-e CLICKHOUSE_DATABASE='system' \
-e CLICKHOUSE_POOL_SIZE='10' \
-e CLICKHOUSE_POOL_TIMEOUT='300000' \
-e CLICKHOUSE_POOL_CLEANUP_INTERVAL='60000'
VariableDefaultDescription
CLICKHOUSE_MAX_EXECUTION_TIME60Query timeout in seconds
CLICKHOUSE_TZTimezone for ClickHouse queries
CLICKHOUSE_DATABASEsystemDefault database
CLICKHOUSE_POOL_SIZE10Connection pool size
CLICKHOUSE_POOL_TIMEOUT300000Pool acquire timeout (ms)
CLICKHOUSE_POOL_CLEANUP_INTERVAL60000Pool cleanup interval (ms)

Feature permissions

Features are all public and enabled by default. Override only what differs.

-e CHM_DISABLED_FEATURES='peerdb,actions'

# Require authentication for specific features
-e CHM_AUTH_REQUIRED_FEATURES='agent,settings,mcp'

# Fine-grained per feature
-e CHM_FEATURE_AGENT_ACCESS='authenticated' \
-e CHM_FEATURE_SETTINGS_ENABLED='false'
docker run -d --name chmonitor -p 3000:3000 \
  -e CLICKHOUSE_HOST='http://clickhouse:8123' \
  -e CLICKHOUSE_USER='monitoring' \
  -e CLICKHOUSE_PASSWORD='change-me' \
  -e CHM_CONFIG_FILE='/config/chmonitor.toml' \
  -v /path/to/chmonitor.toml:/config/chmonitor.toml:ro \
  ghcr.io/chmonitor/chmonitor:vX.Y.Z

Example chmonitor.toml:

[features.agent]
access = "authenticated"

[features.settings]
enabled = false

[features.mcp]
access = "authenticated"

Feature ids: overview, agent, insights, health, queries, tables, metrics, dashboard, security, logs, settings, cluster, operations, actions, mcp, docs, about.

Authentication

Open access:

-e CHM_AUTH_PROVIDER='none'

Can combine with any provider:

-e CHM_API_KEY_SECRET='a-long-random-secret'

Issue tokens at POST /api/v1/auth/api-key. Use Authorization: Bearer chm_... on API calls.

-e CHM_AUTH_PROVIDER='clerk' \
-e CHM_CLERK_PUBLISHABLE_KEY='pk_live_...' \
-e CLERK_SECRET_KEY='sk_live_...'

Set the canonical CHM_* names once — the client VITE_AUTH_PROVIDER / VITE_CLERK_PUBLISHABLE_KEY derive from them at build time.

Clerk needs a custom build on Docker

The client half of these (CHM_AUTH_PROVIDER, CHM_CLERK_PUBLISHABLE_KEY) is inlined at build time. The pre-built GHCR image is built with auth none, so enabling Clerk requires building a custom image with these set at build time, then passing them (plus CLERK_SECRET_KEY) at runtime. See Authentication.

Cloudflare Access:

-e CHM_AUTH_PROVIDER='proxy' \
-e CHM_CF_ACCESS_TEAM_DOMAIN='https://yourteam.cloudflareaccess.com' \
-e CHM_CF_ACCESS_AUD='<audience-tag>'

nginx / k8s sidecar:

-e CHM_AUTH_PROVIDER='proxy' \
-e CHM_PROXY_AUTH_HEADER='X-Forwarded-User' \
-e CHM_PROXY_AUTH_SECRET='a-long-random-secret'

Without CHM_PROXY_AUTH_SECRET, the trusted-header provider is disabled. Put chmonitor behind a reverse proxy that sets the header.

AI agent

-e LLM_API_KEY='sk-...' \
-e LLM_API_BASE='https://openrouter.ai/api/v1' \
-e LLM_MODEL='openrouter/free' \
-e AGENT_API_TOKEN='bearer-token-for-api-v1-agent' \
-e AGENT_ENABLE_CONTROL_TOOLS='false'

Keep LLM_API_KEY server-side

Never put LLM_API_KEY in a VITE_* variable — those are baked into browser JS.

Set CHM_FEATURE_AGENT_ACCESS=authenticated to require login before using the agent.

Conversation store

Server-side persistence requires CHM_FEATURE_CONVERSATION_DB=true baked into the image at build time (its client VITE_FEATURE_CONVERSATION_DB is derived at build). The pre-built image from GHCR has this disabled by default. Build a custom image with this flag set if you need server-side persistence on Docker.

At runtime, set the backend via CONVERSATION_STORE_BACKEND. Available backends on Docker: postgres, agentstate, memory.

-e CONVERSATION_STORE_BACKEND='postgres' \
-e DATABASE_URL='postgresql://user:pass@host:5432/dbname'
-e CONVERSATION_STORE_BACKEND='agentstate' \
-e AGENTSTATE_API_KEY='as_live_...' \
-e AGENTSTATE_BASE_URL='https://agentstate.app/api'

D1 and Durable Object stores are Cloudflare-only. When CONVERSATION_STORE_BACKEND is unset and AGENTSTATE_API_KEY is present, AgentState is selected automatically; if DATABASE_URL is set, Postgres is used instead.

Health alerting

The health sweep runs at GET /api/cron/health-sweep. On Docker, call it from an external cron or monitoring tool.

-e HEALTH_ALERT_ENABLED='true' \
-e HEALTH_ALERT_WEBHOOK_URL='https://hooks.slack.com/services/...' \
-e HEALTH_ALERT_MIN_SEVERITY='warning' \
-e CRON_SECRET='a-random-secret'

Call the endpoint with Authorization: Bearer <CRON_SECRET>. Prefer the header over a query string to avoid the secret appearing in server logs.

Branding

These are build-time variables (inlined at build). They work when running the pre-built image only if the image was built with them.

-e VITE_TITLE_SHORT='MyCluster' \
-e VITE_LOGO='/logo.png' \
-e VITE_MEASUREMENT_ID='G-XXXXXXXXXX'

These are inlined at build time — pass them as -e flags when building a custom image, not when running the pre-built image.

Upgrading

Pull the new tag

docker pull ghcr.io/chmonitor/chmonitor:vX.Y.Z

Stop and remove the old container

docker rm -f chmonitor

Start with the same env vars

docker run -d --name chmonitor -p 3000:3000 \
  -e CLICKHOUSE_HOST='...' \
  ... \
  ghcr.io/chmonitor/chmonitor:vX.Y.Z

Or with Compose: update the image: tag, then docker compose up -d --force-recreate.

For breaking changes between major versions, see Migrating to v0.3.

Troubleshooting

localhost resolves to the container

localhost inside the container resolves to the container itself, not the host. Use host.docker.internal.

  • Empty pages usually mean the ClickHouse user lacks grants on system tables. Grant SELECT on system.*.
  • If pages load but charts are empty, check CLICKHOUSE_MAX_EXECUTION_TIME — the default is 60 s.

On this page