Docker
Run chmonitor as a Docker container. The image is published to GitHub Container Registry.
Image: ghcr.io/duyet/chmonitor:vX.Y.Z — replace vX.Y.Z with a real release tag.
All releases: https://github.com/duyet/clickhouse-monitoring/pkgs/container/chmonitor
The legacy name ghcr.io/duyet/clickhouse-monitoring also works and points at the same build.
Quick start
Section titled “Quick start”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/duyet/chmonitor:vX.Y.ZOpen http://localhost:3000.
--add-host is needed on Linux when ClickHouse runs on the Docker host (not needed on Docker Desktop for Mac/Windows).
Docker Compose
Section titled “Docker Compose”services: chmonitor: image: ghcr.io/duyet/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: 3If 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.
Configure
Section titled “Configure”All configuration is via environment variables passed as -e KEY=VALUE or in a Compose environment: block. You can also mount a config file (see Feature permissions below).
ClickHouse connection
Section titled “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/duyet/chmonitor:vX.Y.Z| Variable | Default | Description |
|---|---|---|
CLICKHOUSE_HOST | — (required) | ClickHouse URL(s), comma-separated for multi-host |
CLICKHOUSE_USER | default | Username(s), same count as HOST |
CLICKHOUSE_PASSWORD | "" | Password(s), same count as HOST |
CLICKHOUSE_NAME | — | Friendly label(s) shown in the host switcher |
Multiple hosts
Section titled “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
Section titled “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'| Variable | Default | Description |
|---|---|---|
CLICKHOUSE_MAX_EXECUTION_TIME | 60 | Query timeout in seconds |
CLICKHOUSE_TZ | — | Timezone for ClickHouse queries |
CLICKHOUSE_DATABASE | system | Default database |
CLICKHOUSE_POOL_SIZE | 10 | Connection pool size |
CLICKHOUSE_POOL_TIMEOUT | 300000 | Pool acquire timeout (ms) |
CLICKHOUSE_POOL_CLEANUP_INTERVAL | 60000 | Pool cleanup interval (ms) |
Feature permissions
Section titled “Feature permissions”Features are all public and enabled by default. Override only what differs.
Via env vars:
## Disable a feature entirely-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'Via a mounted config file (TOML):
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/duyet/chmonitor:vX.Y.ZExample 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
Section titled “Authentication”None (default) — open access:
-e CHM_AUTH_PROVIDER='none'API key layer (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.
Clerk:
-e CHM_AUTH_PROVIDER='clerk' \-e VITE_AUTH_PROVIDER='clerk' \-e VITE_CLERK_PUBLISHABLE_KEY='pk_live_...' \-e CLERK_SECRET_KEY='sk_live_...'VITE_* vars are build-time inlined; if you run the pre-built image, these must match what the image was built with. For a custom build, set them at build time. See Authentication.
Proxy — Cloudflare Access:
-e CHM_AUTH_PROVIDER='proxy' \-e CHM_CF_ACCESS_TEAM_DOMAIN='https://yourteam.cloudflareaccess.com' \-e CHM_CF_ACCESS_AUD='<audience-tag>'Proxy — trusted header (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
Section titled “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 it in a VITE_* variable.
Set CHM_FEATURE_AGENT_ACCESS=authenticated to require login before using the agent.
Conversation store
Section titled “Conversation store”Server-side persistence requires VITE_FEATURE_CONVERSATION_DB=true to be baked into the image at build time (it is a VITE_* build-time variable). 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.
Postgres store (recommended on Docker):
-e CONVERSATION_STORE_BACKEND='postgres' \-e DATABASE_URL='postgresql://user:pass@host:5432/dbname'AgentState store:
-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
Section titled “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
Section titled “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
Section titled “Upgrading”- Pull the new image tag:
docker pull ghcr.io/duyet/chmonitor:vX.Y.Z- Stop and remove the old container:
docker rm -f chmonitor- Start a new container with the same env vars:
docker run -d --name chmonitor -p 3000:3000 \ -e CLICKHOUSE_HOST='...' \ ... \ ghcr.io/duyet/chmonitor:vX.Y.ZOr with Compose: update the image: tag, then docker compose up -d --force-recreate.
For breaking changes between major versions, see Migrating to v0.3.
Common problems
Section titled “Common problems”localhostinside the container resolves to the container itself, not the host. Usehost.docker.internal.- Empty pages usually mean the ClickHouse user lacks grants on system tables. Grant
SELECTonsystem.*. - If pages load but charts are empty, check
CLICKHOUSE_MAX_EXECUTION_TIME— the default is 60 s.