# 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

```bash
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.Z
```

Open `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

```yaml
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: 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`.

## 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](#feature-permissions) below).

### ClickHouse connection

```bash
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

`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.

```bash
-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

```bash
-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

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

**Via env vars:**

```bash
## 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):**

```bash
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.Z
```

Example `chmonitor.toml`:

```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

**None (default) — open access:**

```bash
-e CHM_AUTH_PROVIDER='none'
```

**API key layer (can combine with any provider):**

```bash
-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:**

```bash
-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](/authentication).

**Proxy — Cloudflare Access:**

```bash
-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):**

```bash
-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

```bash
-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

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):**

```bash
-e CONVERSATION_STORE_BACKEND='postgres' \
-e DATABASE_URL='postgresql://user:pass@host:5432/dbname'
```

**AgentState store:**

```bash
-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.

```bash
-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.

```bash
-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

1. Pull the new image tag:

```bash
docker pull ghcr.io/duyet/chmonitor:vX.Y.Z
```

2. Stop and remove the old container:

```bash
docker rm -f chmonitor
```

3. Start a new container with the same env vars:

```bash
docker run -d --name chmonitor -p 3000:3000 \
  -e CLICKHOUSE_HOST='...' \
  ... \
  ghcr.io/duyet/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](/migrating/v0-3).

## Common problems

- `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.
