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.
kubectlandHelmon 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.
ReplicatedMergeTreeuses 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
$ kubectl wait --for=condition=Ready pods --all -n cert-manager --timeout=120s
2. Install the ClickHouse Operator:
$ helm install clickhouse-operator oci://ghcr.io/clickhouse/clickhouse-operator-helm \
--create-namespace \
--namespace clickhouse-operator-system
$ kubectl wait -n clickhouse-operator-system deployment/clickhouse-operator-controller-manager --for=condition=Available --timeout=120s
Create the Database Cluster
1. Create a dedicated namespace:
$ kubectl create namespace clickhouse-db
Deploy Keeper
2. Save the Keeper cluster manifest:
$ nano keeper-cluster.yaml
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
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
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
5. Save the TLS manifest:
$ nano clickhouse-tls.yaml
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
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
7. Save the ClickHouse cluster manifest:
$ nano clickhouse-cluster.yaml
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
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
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
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)
3. Confirm the replication macros:
SELECT * FROM system.macros;
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');
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"
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)
SELECT version();
External access — needs a LoadBalancer service:
$ nano clickhouse-lb.yaml
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
$ kubectl apply -f clickhouse-lb.yaml
$ kubectl get svc clickhouse-external -n clickhouse-db
On your workstation, create a client TLS config that trusts the self-signed cert:
$ nano clickhouse-client-tls.xml
<config>
<openSSL>
<client>
<verificationMode>none</verificationMode>
<invalidCertificateHandler>
<name>AcceptCertificateHandler</name>
</invalidCertificateHandler>
</client>
</openSSL>
</config>
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)
Note:
verificationMode: noneis fine for testing self-signed certs. For production, issue a cert-manager certificate covering the external domain, dropverificationMode, setcaConfigto the CA path, and swapAcceptCertificateHandlerforRejectCertificateHandler. Restrict the LoadBalancer to trusted IPs via firewall rules.
Scaling and Maintenance
-
Scaling: bump
replicasin theClickHouseClustermanifest 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)