DEV Community

Cover image for Deploying ClickHouse on Kubernetes
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya • Originally published at docs.vultr.com

Deploying ClickHouse on Kubernetes

ClickHouse is an open-source, column-oriented database built for OLAP workloads, running complex analytical queries across large datasets with sub-second latency. The official ClickHouse Kubernetes Operator manages ClickHouse and Keeper clusters as CRDs, automating setup, scaling, and upgrades. This guide deploys the operator, a 3-node Keeper coordination group, and a 2-replica ClickHouse cluster with TLS, then verifies replication and connects both internally and externally. By the end, you'll have a ClickHouse cluster running on Kubernetes with encrypted client connections.

Prerequisite: Kubernetes cluster sized for 3 Keeper pods + 2 ClickHouse replicas, production clusters want 3+ nodes with 8 vCPUs and 16 GB RAM each. kubectl and Helm on your workstation.


Architecture

  • ClickHouse Keeper — 3-node coordination group; tracks replication and elects a leader. Each node persists to its own PVC.
  • ClickHouse Cluster — 2-replica database cluster. ReplicatedMergeTree uses Keeper to sync data across replicas.
  • Load Balancer — optional; internal cluster traffic uses the service name directly, no load balancer needed.

The Operator watches ClickHouse CRDs and reconciles StatefulSets, Services, and storage to match.


Install the Operator

1. Install cert-manager (the operator needs it to issue webhook TLS certs):

$ helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \
    --create-namespace \
    --namespace cert-manager \
    --set crds.enabled=true \
    --version v1.19.2
Enter fullscreen mode Exit fullscreen mode
$ kubectl wait --for=condition=Ready pods --all -n cert-manager --timeout=120s
Enter fullscreen mode Exit fullscreen mode

2. Install the ClickHouse Operator:

$ helm install clickhouse-operator oci://ghcr.io/clickhouse/clickhouse-operator-helm \
    --create-namespace \
    --namespace clickhouse-operator-system
Enter fullscreen mode Exit fullscreen mode
$ kubectl wait -n clickhouse-operator-system deployment/clickhouse-operator-controller-manager --for=condition=Available --timeout=120s
Enter fullscreen mode Exit fullscreen mode

Create the Database Cluster

1. Create a dedicated namespace:

$ kubectl create namespace clickhouse-db
Enter fullscreen mode Exit fullscreen mode

Deploy Keeper

2. Save the Keeper cluster manifest:

$ nano keeper-cluster.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: clickhouse.com/v1alpha1
kind: KeeperCluster
metadata:
  name: clickhouse-keeper
  namespace: clickhouse-db
spec:
  replicas: 3
  dataVolumeClaimSpec:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 40Gi
  podTemplate:
    affinity:
      podAntiAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchExpressions:
              - key: clickhouse.com/app
                operator: In
                values:
                - clickhouse-keeper
            topologyKey: kubernetes.io/hostname
  containerTemplate:
    resources:
      requests:
        cpu: "500m"
        memory: 1Gi
      limits:
        cpu: "1"
        memory: 2Gi
Enter fullscreen mode Exit fullscreen mode

The podAntiAffinity rule spreads Keeper nodes across different machines so the group survives a single node failure. Adjust storage to your platform's minimum volume size, and pin an explicit StorageClass if your cluster has no default.

3. Apply and wait for readiness:

$ kubectl apply -f keeper-cluster.yaml
$ kubectl wait --for=condition=Ready pods -l app.kubernetes.io/instance=clickhouse-keeper-keeper -n clickhouse-db --timeout=300s
Enter fullscreen mode Exit fullscreen mode

Deploy ClickHouse

4. Generate a password secret without echoing it to the terminal:

$ kubectl create secret generic clickhouse-password --from-literal=password="$(openssl rand -base64 16)" -n clickhouse-db
Enter fullscreen mode Exit fullscreen mode

5. Save the TLS manifest:

$ nano clickhouse-tls.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: clickhouse-issuer
  namespace: clickhouse-db
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: clickhouse-tls
  namespace: clickhouse-db
spec:
  secretName: clickhouse-tls-cert
  issuerRef:
    name: clickhouse-issuer
    kind: Issuer
  dnsNames:
    - clickhouse-clickhouse-headless
    - clickhouse-clickhouse-headless.clickhouse-db
    - clickhouse-clickhouse-headless.clickhouse-db.svc
    - clickhouse-clickhouse-headless.clickhouse-db.svc.cluster.local
Enter fullscreen mode Exit fullscreen mode

6. Apply and wait for issuance:

$ kubectl apply -f clickhouse-tls.yaml
$ kubectl wait --for=condition=Ready certificate clickhouse-tls -n clickhouse-db --timeout=120s
Enter fullscreen mode Exit fullscreen mode

7. Save the ClickHouse cluster manifest:

$ nano clickhouse-cluster.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: clickhouse.com/v1alpha1
kind: ClickHouseCluster
metadata:
  name: clickhouse
  namespace: clickhouse-db
spec:
  replicas: 2
  settings:
    defaultUserPassword:
      passwordType: password
      secret:
        name: clickhouse-password
        key: password
    tls:
      enabled: true
      serverCertSecret:
        name: clickhouse-tls-cert
  dataVolumeClaimSpec:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 50Gi
  podTemplate:
    affinity:
      podAntiAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchExpressions:
              - key: clickhouse.com/app
                operator: In
                values:
                - clickhouse-clickhouse
            topologyKey: kubernetes.io/hostname
  keeperClusterRef:
    name: clickhouse-keeper
  containerTemplate:
    resources:
      requests:
        cpu: "1"
        memory: 2Gi
      limits:
        cpu: "2"
        memory: 4Gi
Enter fullscreen mode Exit fullscreen mode

8. Apply and wait for readiness:

$ kubectl apply -f clickhouse-cluster.yaml
$ kubectl wait --for=condition=Ready pods -l app.kubernetes.io/instance=clickhouse-clickhouse -n clickhouse-db --timeout=300s
Enter fullscreen mode Exit fullscreen mode

Verify the Deployment

1. Check both pod groups and the cluster resource:

$ kubectl get pods -l app.kubernetes.io/instance=clickhouse-keeper-keeper -n clickhouse-db
$ kubectl get pods -l clickhouse.com/role=clickhouse-server -n clickhouse-db
$ kubectl get clickhousecluster clickhouse -n clickhouse-db
Enter fullscreen mode Exit fullscreen mode

The last command's READY column reads True once all replicas are up.

2. Connect to the first pod with the built-in client:

$ kubectl exec -it clickhouse-clickhouse-0-0-0 -n clickhouse-db -- clickhouse-client --password $(kubectl get secret clickhouse-password -n clickhouse-db -o jsonpath='{.data.password}' | base64 -d)
Enter fullscreen mode Exit fullscreen mode

3. Confirm the replication macros:

SELECT * FROM system.macros;
Enter fullscreen mode Exit fullscreen mode

4. Create a replicated table and insert data:

CREATE TABLE default.page_views (
    event_time DateTime,
    user_id UInt64,
    page_path String
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')
ORDER BY (event_time, user_id);

INSERT INTO default.page_views VALUES
    (now() - INTERVAL 1 HOUR, 1001, '/home'),
    (now() - INTERVAL 30 MINUTE, 1002, '/products'),
    (now(), 1001, '/checkout');
Enter fullscreen mode Exit fullscreen mode

Type exit to leave the session.

5. Confirm replication landed on the second pod:

$ kubectl exec -it clickhouse-clickhouse-0-1-0 -n clickhouse-db -- clickhouse-client --password $(kubectl get secret clickhouse-password -n clickhouse-db -o jsonpath='{.data.password}' | base64 -d) -q "SELECT count() FROM default.page_views"
Enter fullscreen mode Exit fullscreen mode

Output 3 confirms the rows replicated to the other pod.


Connect to the Database

The operator creates a headless service, clickhouse-clickhouse-headless, on port 9000 (plain TCP) and 9440 (TLS).

Internal access — from a pod inside the cluster:

$ kubectl run clickhouse-test \
    --image=clickhouse/clickhouse-server \
    --rm -it --restart=Never \
    -n clickhouse-db \
    -- clickhouse-client \
    --host clickhouse-clickhouse-headless \
    --port 9000 \
    --password $(kubectl get secret clickhouse-password -n clickhouse-db -o jsonpath='{.data.password}' | base64 -d)
Enter fullscreen mode Exit fullscreen mode
SELECT version();
Enter fullscreen mode Exit fullscreen mode

External access — needs a LoadBalancer service:

$ nano clickhouse-lb.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Service
metadata:
  name: clickhouse-external
  namespace: clickhouse-db
spec:
  type: LoadBalancer
  selector:
    clickhouse.com/role: clickhouse-server
  ports:
  - port: 9440
    targetPort: 9440
    protocol: TCP
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f clickhouse-lb.yaml
$ kubectl get svc clickhouse-external -n clickhouse-db
Enter fullscreen mode Exit fullscreen mode

On your workstation, create a client TLS config that trusts the self-signed cert:

$ nano clickhouse-client-tls.xml
Enter fullscreen mode Exit fullscreen mode
<config>
  <openSSL>
    <client>
      <verificationMode>none</verificationMode>
      <invalidCertificateHandler>
        <name>AcceptCertificateHandler</name>
      </invalidCertificateHandler>
    </client>
  </openSSL>
</config>
Enter fullscreen mode Exit fullscreen mode

Connect using the LoadBalancer's external IP:

$ clickhouse-client --config clickhouse-client-tls.xml --host LB_IP --port 9440 --secure --password $(kubectl get secret clickhouse-password -n clickhouse-db -o jsonpath='{.data.password}' | base64 -d)
Enter fullscreen mode Exit fullscreen mode

Note: verificationMode: none is fine for testing self-signed certs. For production, issue a cert-manager certificate covering the external domain, drop verificationMode, set caConfig to the CA path, and swap AcceptCertificateHandler for RejectCertificateHandler. Restrict the LoadBalancer to trusted IPs via firewall rules.


Scaling and Maintenance

  • Scaling: bump replicas in the ClickHouseCluster manifest and re-apply — the operator handles pod additions/removals and data movement.
  • Backups: ClickHouse has native backup/restore tooling — see the official backup docs.
  • Logging: wire up log collection per the official server-configuration docs to monitor cluster health.

Next Steps

ClickHouse is running with TLS and verified 2-way replication. From here you can:

  • Scale Keeper to 5 nodes for larger clusters needing more coordination headroom
  • Move backups to S3-compatible object storage for offsite retention
  • Front the LoadBalancer with a proper CA-issued certificate before exposing it publicly

For the full guide with additional tips, visit the original article on Vultr Docs.

Top comments (0)