# Kubernetes

Run chmonitor on Kubernetes with the vendored Helm chart or raw kustomize manifests. Both use the same image (`ghcr.io/duyet/chmonitor:vX.Y.Z`), expose port `3000`, run as the non-root `app` user (uid/gid `1001`), and wire the same health probes.

## Quick start with Helm

The chart is published in two registries:

| Registry | Install command |
|----------|----------------|
| **Helm repo** (Cloudflare Pages) | `helm repo add chmonitor https://charts.chmonitor.dev` |
| **OCI** (GHCR) | `helm install my-chm oci://ghcr.io/duyet/chmonitor --version X.Y.Z` |

### From the Helm repository

```bash
helm repo add chmonitor https://charts.chmonitor.dev
helm repo update

helm install my-chm chmonitor/chmonitor \
  --set clickhouse.host="https://clickhouse.example.com:8443" \
  --set clickhouse.user="monitoring" \
  --set clickhouse.password="change-me"

kubectl port-forward svc/my-chm-chmonitor 3000:3000
## open http://localhost:3000
```

Upgrade and uninstall:

```bash
helm upgrade my-chm chmonitor/chmonitor -f values.yaml
helm uninstall my-chm
```

### From the OCI registry (GHCR)

Replace `vX.Y.Z` with the latest release tag from [GitHub Releases](https://github.com/duyet/clickhouse-monitoring/releases).

```bash
helm install my-chm oci://ghcr.io/duyet/chmonitor --version vX.Y.Z \
  --set clickhouse.host="https://clickhouse.example.com:8443" \
  --set clickhouse.user="monitoring" \
  --set clickhouse.password="change-me"
```

Pull and inspect the chart before installing:

```bash
helm pull oci://ghcr.io/duyet/chmonitor --version vX.Y.Z --untar
helm show values ./chmonitor
```

### From source (vendored chart)

Alternatively, clone the repo and install the chart directly — useful when you want to patch the chart before installing:

```bash
git clone https://github.com/duyet/clickhouse-monitoring.git
cd clickhouse-monitoring

helm install my-chm ./deploy/helm/chmonitor \
  --set clickhouse.host="https://clickhouse.example.com:8443" \
  --set clickhouse.user="monitoring" \
  --set clickhouse.password="change-me"
```

### With a values file

```bash
helm install my-chm chmonitor/chmonitor -f values.yaml
```

Example `values.yaml`:

```yaml
image:
  tag: "vX.Y.Z"   # use the latest release tag from https://github.com/duyet/clickhouse-monitoring/releases

clickhouse:
  host: "https://clickhouse.example.com:8443"
  user: "monitoring"
  password: "change-me"

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: chmonitor.example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  requests:
    cpu: 100m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi
```

## Quick start with kustomize

```bash
## Review rendered output first
kubectl kustomize deploy/kubernetes/base

## Apply
kubectl apply -k deploy/kubernetes/base

kubectl port-forward svc/chmonitor 3000:3000
```

Keep environment differences in an overlay:

```yaml
## deploy/kubernetes/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: monitoring
resources:
  - ../../base
images:
  - name: ghcr.io/duyet/chmonitor
    newTag: vX.Y.Z
replicas:
  - name: chmonitor
    count: 2
```

## Configure

### ClickHouse connection

Store credentials in a Secret, not a ConfigMap.

```bash
kubectl create secret generic chmonitor-clickhouse \
  --from-literal=CLICKHOUSE_HOST='https://clickhouse.example.com:8443' \
  --from-literal=CLICKHOUSE_USER='monitoring' \
  --from-literal=CLICKHOUSE_PASSWORD='change-me'
```

Reference it in your Deployment:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: chmonitor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: chmonitor
  template:
    metadata:
      labels:
        app: chmonitor
    spec:
      containers:
        - name: chmonitor
          image: ghcr.io/duyet/chmonitor:vX.Y.Z
          ports:
            - containerPort: 3000
          envFrom:
            - secretRef:
                name: chmonitor-clickhouse
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /api/healthz
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
```

#### 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
kubectl create secret generic chmonitor-clickhouse \
  --from-literal=CLICKHOUSE_HOST='https://ch1:8443,https://ch2:8443' \
  --from-literal=CLICKHOUSE_USER='monitoring,monitoring' \
  --from-literal=CLICKHOUSE_PASSWORD='pass1,pass2' \
  --from-literal=CLICKHOUSE_NAME='shard-1,shard-2'
```

#### Query / pool tuning

Add these to a ConfigMap (non-secret values):

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: chmonitor-config
data:
  CLICKHOUSE_MAX_EXECUTION_TIME: "30"
  CLICKHOUSE_TZ: "UTC"
  CLICKHOUSE_DATABASE: "system"
  CLICKHOUSE_POOL_SIZE: "10"
  CLICKHOUSE_POOL_TIMEOUT: "300000"
  CLICKHOUSE_POOL_CLEANUP_INTERVAL: "60000"
```

Reference both in the Deployment:

```yaml
envFrom:
  - secretRef:
      name: chmonitor-clickhouse
  - configMapRef:
      name: chmonitor-config
```

### Feature permissions

**Via env in ConfigMap:**

```yaml
## chmonitor-config ConfigMap additions
CHM_DISABLED_FEATURES: "peerdb,actions"
CHM_AUTH_REQUIRED_FEATURES: "agent,settings,mcp"
CHM_FEATURE_AGENT_ACCESS: "authenticated"
```

**Via mounted config file (recommended for complex rules):**

Create a ConfigMap with a TOML file:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: chmonitor-features
data:
  chmonitor.toml: |
    [features.agent]
    access = "authenticated"

    [features.settings]
    enabled = false

    [features.mcp]
    access = "authenticated"

    [features.actions]
    enabled = false
```

Mount it and point `CHM_CONFIG_FILE` at it:

```yaml
containers:
  - name: chmonitor
    image: ghcr.io/duyet/chmonitor:vX.Y.Z
    env:
      - name: CHM_CONFIG_FILE
        value: /config/chmonitor.toml
    volumeMounts:
      - name: features-config
        mountPath: /config
        readOnly: true
volumes:
  - name: features-config
    configMap:
      name: chmonitor-features
```

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

### Authentication

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

```yaml
## in ConfigMap
CHM_AUTH_PROVIDER: "none"
```

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

```bash
kubectl create secret generic chmonitor-auth \
  --from-literal=CHM_API_KEY_SECRET='a-long-random-secret'
```

**Clerk:**

```bash
kubectl create secret generic chmonitor-clerk \
  --from-literal=CLERK_SECRET_KEY='sk_live_...'
```

```yaml
## in ConfigMap (these are build-time vars; must match the image build)
VITE_AUTH_PROVIDER: "clerk"
VITE_CLERK_PUBLISHABLE_KEY: "pk_live_..."
CHM_AUTH_PROVIDER: "clerk"
```

Note: `VITE_*` vars are inlined at build time. For a pre-built image, these must match what the image was built with. See [Authentication](/authentication).

**Proxy — Cloudflare Access:**

```yaml
## in ConfigMap
CHM_AUTH_PROVIDER: "proxy"
CHM_CF_ACCESS_TEAM_DOMAIN: "https://yourteam.cloudflareaccess.com"
CHM_CF_ACCESS_AUD: "<audience-tag>"
```

**Proxy — trusted header (nginx ingress / sidecar):**

```bash
kubectl create secret generic chmonitor-proxy \
  --from-literal=CHM_PROXY_AUTH_SECRET='a-long-random-secret'
```

```yaml
## in ConfigMap
CHM_AUTH_PROVIDER: "proxy"
CHM_PROXY_AUTH_HEADER: "X-Forwarded-User"
CHM_PROXY_SHARED_SECRET_HEADER: "X-Chm-Proxy-Secret"
```

Without `CHM_PROXY_AUTH_SECRET`, the trusted-header provider is disabled. Configure your ingress to set the header and the secret. See [Authentication](/authentication).

### AI agent

```bash
kubectl create secret generic chmonitor-agent \
  --from-literal=LLM_API_KEY='sk-...' \
  --from-literal=AGENT_API_TOKEN='bearer-token-for-agent-api'
```

```yaml
## in ConfigMap
LLM_API_BASE: "https://openrouter.ai/api/v1"
LLM_MODEL: "openrouter/free"
AGENT_ENABLE_CONTROL_TOOLS: "false"
```

Set `CHM_FEATURE_AGENT_ACCESS=authenticated` in the ConfigMap to require login. Keep `LLM_API_KEY` in the Secret — never in a `VITE_*` var or ConfigMap.

### Conversation store

**Default:** browser localStorage — no server config needed.

Server-side persistence requires `VITE_FEATURE_CONVERSATION_DB=true` baked into the image at build time (a `VITE_*` build-time variable). Build a custom image with this flag to enable it. At runtime, set the backend via `CONVERSATION_STORE_BACKEND` in a ConfigMap.

On Kubernetes, use `postgres` or `agentstate` for the conversation store. D1 and Durable Object stores are Cloudflare-only.

**Postgres store (recommended on Kubernetes):**

```bash
kubectl create secret generic chmonitor-postgres \
  --from-literal=DATABASE_URL='postgresql://user:pass@host:5432/dbname'
```

```yaml
## in ConfigMap
CONVERSATION_STORE_BACKEND: "postgres"
```

**AgentState store:**

```bash
kubectl create secret generic chmonitor-agentstate \
  --from-literal=AGENTSTATE_API_KEY='as_live_...'
```

```yaml
## in ConfigMap
CONVERSATION_STORE_BACKEND: "agentstate"
```

### Health alerting

The health sweep runs at `GET /api/cron/health-sweep`. Trigger it from a Kubernetes CronJob:

```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: chmonitor-health-sweep
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: sweep
              image: curlimages/curl:latest
              env:
                - name: CRON_SECRET
                  valueFrom:
                    secretKeyRef:
                      name: chmonitor-cron
                      key: CRON_SECRET
              command:
                - sh
                - -c
                - 'curl -sf -H "Authorization: Bearer $CRON_SECRET" http://chmonitor:3000/api/cron/health-sweep'
          restartPolicy: OnFailure
```

Set the webhook and secret:

```yaml
## in ConfigMap
HEALTH_ALERT_ENABLED: "true"
HEALTH_ALERT_MIN_SEVERITY: "warning"
```

```bash
## Health-alerting credentials live in their own secret so re-running this
## never touches chmonitor-auth (which holds CHM_API_KEY_SECRET).
kubectl create secret generic chmonitor-cron \
  --from-literal=CRON_SECRET='a-random-secret' \
  --from-literal=HEALTH_ALERT_WEBHOOK_URL='https://hooks.slack.com/services/...' \
  --dry-run=client -o yaml | kubectl apply -f -
```

Reference `HEALTH_ALERT_WEBHOOK_URL` from the `chmonitor-cron` secret in the
Deployment using an explicit `secretKeyRef` (do not add the whole cron secret
via `envFrom` — that would also expose `CRON_SECRET` to the app container):

```yaml
env:
  - name: HEALTH_ALERT_WEBHOOK_URL
    valueFrom:
      secretKeyRef:
        name: chmonitor-cron
        key: HEALTH_ALERT_WEBHOOK_URL
```

### Branding

Branding vars are inlined at build time. For a pre-built image, customize by building your own image with these set:

```bash
VITE_TITLE_SHORT=MyCluster
VITE_LOGO=/logo.png
```

These are inlined at build time — set them in CI when building the image, not as runtime env vars.

## Health probes

- **Liveness** — `GET /healthz` — always `200` while the process runs.
- **Readiness** — `GET /api/healthz` — returns `503` when no ClickHouse host is reachable.

## Autoscaling

```yaml
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
```

The dashboard is stateless, so scaling out is safe. The readiness probe keeps traffic off pods until ClickHouse is reachable.

## Secrets management

For GitOps workflows, do not commit real passwords. Use:

- [External Secrets](https://external-secrets.io/) — sync from AWS Secrets Manager, GCP Secret Manager, Vault, etc.
- [SOPS](https://github.com/getsops/sops) — encrypt secrets in Git.
- [Sealed Secrets](https://sealed-secrets.netlify.app/) — encrypt for a specific cluster.

## Upgrading

1. Update the image tag in your `values.yaml` or kustomize overlay.
2. Apply the change:

```bash
## Helm
helm upgrade my-chm ./deploy/helm/chmonitor -f values.yaml

## kustomize
kubectl apply -k deploy/kubernetes/overlays/prod
```

3. Verify the rollout:

```bash
kubectl rollout status deployment/chmonitor
```

For breaking changes between major versions, see [Migrating to v0.3](/migrating/v0-3).

## Validation

```bash
helm lint ./deploy/helm/chmonitor
helm template release ./deploy/helm/chmonitor | kubeconform -strict -summary
kubectl kustomize deploy/kubernetes/base | kubeconform -strict -summary
```
